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 HeaderItem from 'sentry/components/organizations/headerItem';
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 {IconWindow} from 'sentry/icons';
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 & {
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';
customDropdownButton?: (config: {
actions: MenuActions;
isOpen: boolean;
value: string[];
}) => React.ReactElement;
customLoadingIndicator?: React.ReactNode;
detached?: boolean;
disabled?: boolean;
forceEnvironment?: string;
/**
* Show the pin button in the dropdown's header actions
*/
showPin?: boolean;
};
/**
* Environment Selector
*
* Note we only fetch environments when this component is mounted
*/
function EnvironmentSelector({
loadingProjects,
onUpdate,
organization,
projects,
selectedProjects,
value,
alignDropdown,
customDropdownButton,
customLoadingIndicator,
detached,
disabled,
forceEnvironment,
router,
showPin,
}: 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);
};
/**
* Clears all selected environments and updates
*/
const handleClear = () => {
analytics('environmentselector.clear', {
path: getRouteStringFromRoutes(router.routes),
org_id: parseInt(organization.id, 10),
});
setSelectedEnvs([]);
onUpdate([]);
};
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 config = ConfigStore.getConfig();
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 || config.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));
const summary = validatedValue.length
? `${validatedValue.join(', ')}`
: t('All Environments');
if (forceEnvironment !== undefined) {
return (
}
isOpen={false}
locked
>
{forceEnvironment ? forceEnvironment : t('All Environments')}
);
}
if (loadingProjects && customLoadingIndicator) {
return {customLoadingIndicator};
}
if (loadingProjects) {
return (
}
loading={loadingProjects}
hasChanges={false}
hasSelected={false}
isOpen={false}
locked={false}
>
{t('Loading\u2026')}
);
}
return (
{({css}) => (
: undefined
}
menuFooter={({actions}) =>
hasChanges ? (
handleSave(actions)} />
) : null
}
items={environments.map(env => ({
value: env,
searchKey: env,
label: ({inputValue}) => (
{
e.stopPropagation();
toggleCheckbox(env);
}}
>
{env}
),
}))}
>
{({isOpen, actions}) =>
customDropdownButton ? (
customDropdownButton({isOpen, actions, value: validatedValue})
) : (
}
isOpen={isOpen}
hasSelected={value && !!value.length}
onClear={handleClear}
hasChanges={false}
locked={false}
loading={false}
>
{summary}
)
}
)}
);
}
export default withRouter(EnvironmentSelector);
const StyledHeaderItem = styled(HeaderItem)`
height: 100%;
width: 100%;
`;
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)};
`;