import {Fragment, useEffect, useRef, useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import {withRouter, WithRouterProps} from 'react-router';
import {ClassNames} from '@emotion/react';
import styled from '@emotion/styled';
import isEqual from 'lodash/isEqual';
import sortBy from 'lodash/sortBy';
import {MenuActions} from 'sentry/components/deprecatedDropdownMenu';
import DropdownAutoComplete from 'sentry/components/dropdownAutoComplete';
import {MenuFooterChildProps} from 'sentry/components/dropdownAutoComplete/menu';
import {Item} from 'sentry/components/dropdownAutoComplete/types';
import Highlight from 'sentry/components/highlight';
import MultipleSelectorSubmitRow from 'sentry/components/organizations/multipleSelectorSubmitRow';
import PageFilterRow from 'sentry/components/organizations/pageFilterRow';
import PageFilterPinButton from 'sentry/components/organizations/pageFilters/pageFilterPinButton';
import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
import {t} from 'sentry/locale';
import ConfigStore from 'sentry/stores/configStore';
import space from 'sentry/styles/space';
import {Organization, Project} from 'sentry/types';
import {analytics} from 'sentry/utils/analytics';
import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
import theme from 'sentry/utils/theme';
type Props = WithRouterProps & {
customDropdownButton: (config: {
actions: MenuActions;
isOpen: boolean;
value: string[];
}) => React.ReactElement;
customLoadingIndicator: React.ReactNode;
loadingProjects: boolean;
/**
* When menu is closed
*/
onUpdate: (environments: string[]) => void;
organization: Organization;
projects: Project[];
selectedProjects: number[];
/**
* This component must be controlled using a value array
*/
value: string[];
/**
* Aligns dropdown menu to left or right of button
*/
alignDropdown?: 'left' | 'right';
disabled?: boolean;
};
/**
* Environment Selector
*
* Note we only fetch environments when this component is mounted
*/
function EnvironmentSelector({
loadingProjects,
onUpdate,
organization,
projects,
selectedProjects,
value,
alignDropdown,
customDropdownButton,
customLoadingIndicator,
disabled,
router,
}: Props) {
const [selectedEnvs, setSelectedEnvs] = useState(value);
const hasChanges = !isEqual(selectedEnvs, value);
// Update selected envs value on change
useEffect(() => {
setSelectedEnvs(previousSelectedEnvs => {
lastSelectedEnvs.current = previousSelectedEnvs;
return value;
});
}, [value]);
// We keep a separate list of selected environments to use for sorting. This
// allows us to only update it after the list is closed, to avoid the list
// jumping around while selecting projects.
const lastSelectedEnvs = useRef(value);
// Ref to help avoid updating stale selected values
const didQuickSelect = useRef(false);
/**
* Toggle selected state of an environment
*/
const toggleCheckbox = (environment: string) => {
const willRemove = selectedEnvs.includes(environment);
const updatedSelectedEnvs = willRemove
? selectedEnvs.filter(env => env !== environment)
: [...selectedEnvs, environment];
analytics('environmentselector.toggle', {
action: willRemove ? 'removed' : 'added',
path: getRouteStringFromRoutes(router.routes),
org_id: parseInt(organization.id, 10),
});
setSelectedEnvs(updatedSelectedEnvs);
};
const handleSave = (actions: MenuFooterChildProps['actions']) => {
actions.close();
onUpdate(selectedEnvs);
};
const handleMenuClose = () => {
// Only update if there are changes
if (!hasChanges || didQuickSelect.current) {
didQuickSelect.current = false;
return;
}
analytics('environmentselector.update', {
count: selectedEnvs.length,
path: getRouteStringFromRoutes(router.routes),
org_id: parseInt(organization.id, 10),
});
onUpdate(selectedEnvs);
};
const handleQuickSelect = (item: Item) => {
analytics('environmentselector.direct_selection', {
path: getRouteStringFromRoutes(router.routes),
org_id: parseInt(organization.id, 10),
});
const selectedEnvironments = [item.value];
setSelectedEnvs(selectedEnvironments);
onUpdate(selectedEnvironments);
// Track that we just did a click select so we don't trigger an update in
// the close handler.
didQuickSelect.current = true;
};
const {user} = ConfigStore.getState();
const unsortedEnvironments = projects.flatMap(project => {
const projectId = parseInt(project.id, 10);
// Include environments from:
// - all projects if the user is a superuser
// - the requested projects
// - all member projects if 'my projects' (empty list) is selected.
// - all projects if -1 is the only selected project.
if (
(selectedProjects.length === 1 &&
selectedProjects[0] === ALL_ACCESS_PROJECTS &&
project.hasAccess) ||
(selectedProjects.length === 0 && (project.isMember || user.isSuperuser)) ||
selectedProjects.includes(projectId)
) {
return project.environments;
}
return [];
});
const uniqueEnvironments = Array.from(new Set(unsortedEnvironments));
// Sort with the last selected environments at the top
const environments = sortBy(uniqueEnvironments, env => [
!lastSelectedEnvs.current.find(e => e === env),
env,
]);
const validatedValue = value.filter(env => environments.includes(env));
if (loadingProjects) {
return {customLoadingIndicator};
}
return (
{({css}) => (
}
menuFooter={({actions}) =>
hasChanges ? (
handleSave(actions)} />
) : null
}
items={environments.map(env => ({
value: env,
searchKey: env,
label: ({inputValue}) => (
{
e.stopPropagation();
toggleCheckbox(env);
}}
>
{env}
),
}))}
>
{({isOpen, actions}) =>
customDropdownButton({isOpen, actions, value: validatedValue})
}
)}
);
}
export default withRouter(EnvironmentSelector);
const StyledDropdownAutoComplete = styled(DropdownAutoComplete)`
background: ${p => p.theme.background};
border: 1px solid ${p => p.theme.border};
position: absolute;
top: 100%;
${p =>
!p.detached &&
`
margin-top: 0;
border-radius: ${p.theme.borderRadiusBottom};
`};
`;
const StyledPinButton = styled(PageFilterPinButton)`
margin: 0 ${space(1)};
`;