Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): Add project id to URL when visiting Issue Details directly #13875

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ import withProjects from 'app/utils/withProjects';

import {getStateFromQuery} from './utils';

function getProjectIdFromProject(project) {
return parseInt(project.id, 10);
}

class GlobalSelectionHeader extends React.Component {
static propTypes = {
organization: SentryTypes.Organization,
Expand All @@ -42,6 +46,15 @@ class GlobalSelectionHeader extends React.Component {
* List of projects to display in project selector
*/
projects: PropTypes.arrayOf(SentryTypes.Project).isRequired,

/**
* A project will be forced from parent component (selection is disabled, and if user
* does not have multi-project support enabled, it will not try to auto select a project).
*
* Project will be specified in the prop `forceProject` (since its data is async)
*/
shouldForceProject: PropTypes.bool,

/**
* If a forced project is passed, selection is disabled
*/
Expand All @@ -52,20 +65,40 @@ class GlobalSelectionHeader extends React.Component {
*/
selection: SentryTypes.GlobalSelection,

// Display Environment selector?
/**
* Display Environment selector?
*/
showEnvironmentSelector: PropTypes.bool,

// Display Environment selector?
/**
* Display Environment selector?
*/
showDateSelector: PropTypes.bool,

// Disable automatic routing
/**
* Disable automatic routing
*/
hasCustomRouting: PropTypes.bool,

// Reset these URL params when we fire actions
// (custom routing only)
/**
* Reset these URL params when we fire actions
* (custom routing only)
*/
resetParamsOnChange: PropTypes.arrayOf(PropTypes.string),

// Props passed to child components //
/**
* GlobalSelectionStore is not always initialized (e.g. Group Details) before this is rendered
*
* This component intentionally attempts to sync store --> URL Parameter
* only when mounted, except when this prop changes.
*
* XXX: This comes from GlobalSelectionStore and currently does not reset,
* so it happens at most once. Can add a reset as needed.
*/
forceUrlSync: PropTypes.bool,

/// Props passed to child components ///

/**
* Show absolute date selectors
*/
Expand All @@ -75,15 +108,6 @@ class GlobalSelectionHeader extends React.Component {
*/
showRelative: PropTypes.bool,

// GlobalSelectionStore is not always initialized (e.g. Group Details) before this is rendered
//
// This component intentionally attempts to sync store --> URL Parameter
// only when mounted, except when this prop changes.
//
// XXX: This comes from GlobalSelectionStore and currently does not reset,
// so it happens at most once. Can add a reset as needed.
forceUrlSync: PropTypes.bool,

// Callbacks //
onChangeProjects: PropTypes.func,
onUpdateProjects: PropTypes.func,
Expand All @@ -110,7 +134,14 @@ class GlobalSelectionHeader extends React.Component {
return;
}

const {location, params, organization, selection} = this.props;
const {
location,
params,
organization,
selection,
shouldForceProject,
forceProject,
} = this.props;

const hasMultipleProjectFeature = this.hasMultipleProjectSelection();

Expand All @@ -133,12 +164,7 @@ class GlobalSelectionHeader extends React.Component {
if (hasMultipleProjectFeature) {
updateProjects(requestedProjects);
} else {
const allowedProjects =
requestedProjects.length > 0
? requestedProjects.slice(0, 1)
: this.getFirstProject();
updateProjects(allowedProjects);
updateParams({project: allowedProjects}, this.getRouter());
this.enforceSingleProject({requestedProjects, shouldForceProject, forceProject});
}
} else if (params && params.orgId === organization.slug) {
// Otherwise, if organization has NOT changed,
Expand All @@ -147,19 +173,12 @@ class GlobalSelectionHeader extends React.Component {
// e.g. when switching to a new view that uses this component,
// update URL parameters to reflect current store
const {datetime, environments, projects} = selection;
const otherParams = {environment: environments, ...datetime};

if (hasMultipleProjectFeature || projects.length === 1) {
updateParamsWithoutHistory(
{project: projects, environment: environments, ...datetime},
this.getRouter()
);
updateParamsWithoutHistory({project: projects, ...otherParams}, this.getRouter());
} else {
const allowedProjects = this.getFirstProject();
updateProjects(allowedProjects);
updateParams(
{project: allowedProjects, environment: environments, ...datetime},
this.getRouter()
);
this.enforceSingleProject({shouldForceProject, forceProject}, otherParams);
}
}
}
Expand Down Expand Up @@ -216,13 +235,32 @@ class GlobalSelectionHeader extends React.Component {
}

componentDidUpdate(prevProps) {
const {hasCustomRouting, location, forceUrlSync, selection} = this.props;
const {
hasCustomRouting,
location,
selection,
forceUrlSync,
forceProject,
} = this.props;

if (hasCustomRouting) {
return;
}

// Kind of gross
// This means that previously forceProject was falsey (e.g. loading) and now
// we have the project to force.
//
// If user does not have multiple project selection, we need to save the forced
// project into the store (if project is not in URL params), otherwise
// there will be weird behavior in this component since it just picks a project
if (!this.hasMultipleProjectSelection() && forceProject && !prevProps.forceProject) {
// Make sure a project isn't specified in query param already, since it should take precendence
const {project} = getStateFromQuery(location.query);
if (!project) {
this.enforceSingleProject({forceProject});
}
}

if (forceUrlSync && !prevProps.forceUrlSync) {
const {project, environment} = getStateFromQuery(location.query);

Expand All @@ -249,6 +287,38 @@ class GlobalSelectionHeader extends React.Component {
return new Set(this.props.organization.features).has('global-views');
};

/**
* If user does not have access to `global-views` (e.g. multi project select), then
* we update URL params with 1) `props.forceProject`, 2) requested projects from URL params,
* 3) first project user is a member of from org
*/
enforceSingleProject = (
{requestedProjects, shouldForceProject, forceProject} = {},
otherParams
) => {
let newProject;

// This is the case where we *want* to force project, but we are still loading
// the forced project's details
if (shouldForceProject && !forceProject) {
return;
}

if (forceProject) {
// this takes precendence over the other options
newProject = [getProjectIdFromProject(forceProject)];
} else if (requestedProjects && requestedProjects.length > 0) {
// If there is a list of projects from URL params, select first project from that list
newProject = [requestedProjects[0]];
} else {
// Otherwise, get first project from org that the user is a member of
newProject = this.getFirstProject();
}

updateProjects(newProject);
updateParamsWithoutHistory({project: newProject, ...otherParams}, this.getRouter());
};

/**
* Identifies the query params (that are relevant to this component) that have changed
*
Expand Down Expand Up @@ -390,7 +460,7 @@ class GlobalSelectionHeader extends React.Component {

getFirstProject = () => {
return flatten(this.getProjects())
.map(p => parseInt(p.id, 10))
.map(getProjectIdFromProject)
.slice(0, 1);
};

Expand All @@ -412,6 +482,7 @@ class GlobalSelectionHeader extends React.Component {
render() {
const {
className,
shouldForceProject,
forceProject,
organization,
showAbsolute,
Expand All @@ -430,9 +501,10 @@ class GlobalSelectionHeader extends React.Component {
return (
<Header className={className}>
<HeaderItemPosition>
{forceProject && this.getBackButton()}
{shouldForceProject && this.getBackButton()}
<MultipleProjectSelector
organization={organization}
shouldForceProject={shouldForceProject}
forceProject={forceProject}
projects={projects}
nonMemberProjects={nonMemberProjects}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default class MultipleProjectSelector extends React.PureComponent {
onChange: PropTypes.func,
onUpdate: PropTypes.func,
multi: PropTypes.bool,
shouldForceProject: PropTypes.bool,
forceProject: SentryTypes.Project,
};

Expand Down Expand Up @@ -135,6 +136,7 @@ export default class MultipleProjectSelector extends React.PureComponent {
nonMemberProjects,
multi,
organization,
shouldForceProject,
forceProject,
} = this.props;
const selectedProjectIds = new Set(value);
Expand All @@ -145,14 +147,23 @@ export default class MultipleProjectSelector extends React.PureComponent {
selectedProjectIds.has(parseInt(project.id, 10))
);

return forceProject ? (
// `forceProject` can be undefined if it is loading the project
// We are intentionally using an empty string as its "loading" state

return shouldForceProject ? (
<StyledHeaderItem
icon={<StyledInlineSvg src="icon-project" />}
locked={true}
lockedMessage={t(`This issue is unique to the ${forceProject.slug} project`)}
settingsLink={`/settings/${organization.slug}/projects/${forceProject.slug}/`}
lockedMessage={
forceProject
? t(`This issue is unique to the ${forceProject.slug} project`)
: t('This issue is unique to a project')
}
settingsLink={
forceProject && `/settings/${organization.slug}/projects/${forceProject.slug}/`
}
>
{forceProject.slug}
{forceProject ? forceProject.slug : ''}
</StyledHeaderItem>
) : (
<StyledProjectSelector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ const GroupDetails = createReactClass({
organization={organization}
forceProject={project}
showDateSelector={false}
shouldForceProject
/>
)}
{isLoading ? (
Expand Down
Loading