mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-29 01:15:48 +02:00
<!-- Thanks for creating a PR! To make it easier for reviewers and everyone else to understand what your changes relate to, please add some relevant content to the headings below. Feel free to ignore or delete sections that you don't think are relevant. Thank you! ❤️ --> ## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> <!-- Does it close an issue? Multiple? --> Closes # [1-743](https://linear.app/unleash/issue/1-743/add-cypress-test-for-notifications-happy-path) <!-- (For internal contributors): Does it relate to an issue on public roadmap? --> <!-- Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: # --> ### Important files <!-- PRs can contain a lot of changes, but not all changes are equally important. Where should a reviewer start looking to get an overview of the changes? Are any files particularly important? --> ## Discussion points <!-- Anything about the PR you'd like to discuss before it gets merged? Got any questions or doubts? --> --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
692 lines
26 KiB
TypeScript
692 lines
26 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
IconButton,
|
|
styled,
|
|
Tooltip,
|
|
useMediaQuery,
|
|
useTheme,
|
|
} from '@mui/material';
|
|
import { Add } from '@mui/icons-material';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
|
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
|
import { getCreateTogglePath } from 'utils/routePathHelpers';
|
|
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
|
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
|
|
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
|
import { IProject } from 'interfaces/project';
|
|
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
|
import useProject from 'hooks/api/getters/useProject/useProject';
|
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
|
import useToast from 'hooks/useToast';
|
|
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
|
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
|
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
|
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
|
import { useSearch } from 'hooks/useSearch';
|
|
import { Search } from 'component/common/Search/Search';
|
|
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
|
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
|
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
|
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
|
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
|
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
|
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
|
import { useEnvironmentsRef } from './hooks/useEnvironmentsRef';
|
|
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
|
import { FeatureToggleSwitch } from './FeatureToggleSwitch/FeatureToggleSwitch';
|
|
import { ActionsCell } from './ActionsCell/ActionsCell';
|
|
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
|
import { useStyles } from './ProjectFeatureToggles.styles';
|
|
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
|
|
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
|
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
|
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
|
import { flexRow } from 'themes/themeStyles';
|
|
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
|
|
import FileDownload from '@mui/icons-material/FileDownload';
|
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
|
|
|
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
|
whiteSpace: 'nowrap',
|
|
}));
|
|
|
|
const StyledSwitchContainer = styled('div', {
|
|
shouldForwardProp: prop => prop !== 'hasWarning',
|
|
})<{ hasWarning?: boolean }>(({ theme, hasWarning }) => ({
|
|
flexGrow: 0,
|
|
...flexRow,
|
|
justifyContent: 'center',
|
|
...(hasWarning && {
|
|
'::before': {
|
|
content: '""',
|
|
display: 'block',
|
|
width: theme.spacing(2),
|
|
},
|
|
}),
|
|
}));
|
|
|
|
interface IProjectFeatureTogglesProps {
|
|
features: IProject['features'];
|
|
environments: IProject['environments'];
|
|
loading: boolean;
|
|
}
|
|
|
|
type ListItemType = Pick<
|
|
IProject['features'][number],
|
|
'name' | 'lastSeenAt' | 'createdAt' | 'type' | 'stale' | 'favorite'
|
|
> & {
|
|
environments: {
|
|
[key in string]: {
|
|
name: string;
|
|
enabled: boolean;
|
|
variantCount: number;
|
|
};
|
|
};
|
|
someEnabledEnvironmentHasVariants: boolean;
|
|
};
|
|
|
|
const staticColumns = ['Actions', 'name', 'favorite'];
|
|
|
|
const defaultSort: SortingRule<string> & {
|
|
columns?: string[];
|
|
} = { id: 'createdAt' };
|
|
|
|
export const ProjectFeatureToggles = ({
|
|
features,
|
|
loading,
|
|
environments: newEnvironments = [],
|
|
}: IProjectFeatureTogglesProps) => {
|
|
const { classes: styles } = useStyles();
|
|
const theme = useTheme();
|
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
|
const [strategiesDialogState, setStrategiesDialogState] = useState({
|
|
open: false,
|
|
featureId: '',
|
|
environmentName: '',
|
|
});
|
|
const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{
|
|
featureId?: string;
|
|
stale?: boolean;
|
|
}>({});
|
|
const [featureArchiveState, setFeatureArchiveState] = useState<
|
|
string | undefined
|
|
>();
|
|
const projectId = useRequiredPathParam('projectId');
|
|
|
|
const { value: storedParams, setValue: setStoredParams } =
|
|
createLocalStorage(
|
|
`${projectId}:FeatureToggleListTable:v1`,
|
|
defaultSort
|
|
);
|
|
const { value: globalStore, setValue: setGlobalStore } =
|
|
useGlobalLocalStorage();
|
|
const navigate = useNavigate();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
|
const environments = useEnvironmentsRef(
|
|
loading ? ['a', 'b', 'c'] : newEnvironments
|
|
);
|
|
const { refetch } = useProject(projectId);
|
|
const { setToastData, setToastApiError } = useToast();
|
|
const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
|
|
usePinnedFavorites(
|
|
searchParams.has('favorites')
|
|
? searchParams.get('favorites') === 'true'
|
|
: globalStore.favorites
|
|
);
|
|
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
|
|
useFeatureApi();
|
|
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
|
const {
|
|
onChangeRequestToggle,
|
|
onChangeRequestToggleClose,
|
|
onChangeRequestToggleConfirm,
|
|
changeRequestDialogDetails,
|
|
} = useChangeRequestToggle(projectId);
|
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
|
const { uiConfig } = useUiConfig();
|
|
|
|
const onToggle = useCallback(
|
|
async (
|
|
projectId: string,
|
|
featureName: string,
|
|
environment: string,
|
|
enabled: boolean
|
|
) => {
|
|
if (isChangeRequestConfigured(environment)) {
|
|
onChangeRequestToggle(featureName, environment, enabled);
|
|
throw new Error('Additional approval required');
|
|
}
|
|
try {
|
|
if (enabled) {
|
|
await toggleFeatureEnvironmentOn(
|
|
projectId,
|
|
featureName,
|
|
environment
|
|
);
|
|
} else {
|
|
await toggleFeatureEnvironmentOff(
|
|
projectId,
|
|
featureName,
|
|
environment
|
|
);
|
|
}
|
|
refetch();
|
|
} catch (error) {
|
|
const message = formatUnknownError(error);
|
|
if (message === ENVIRONMENT_STRATEGY_ERROR) {
|
|
setStrategiesDialogState({
|
|
open: true,
|
|
featureId: featureName,
|
|
environmentName: environment,
|
|
});
|
|
} else {
|
|
setToastApiError(message);
|
|
}
|
|
throw error; // caught when reverting optimistic update
|
|
}
|
|
|
|
setToastData({
|
|
type: 'success',
|
|
title: 'Updated toggle status',
|
|
text: 'Successfully updated toggle status.',
|
|
});
|
|
refetch();
|
|
},
|
|
[
|
|
toggleFeatureEnvironmentOff,
|
|
toggleFeatureEnvironmentOn,
|
|
isChangeRequestConfigured,
|
|
]
|
|
);
|
|
|
|
const onFavorite = useCallback(
|
|
async (feature: IFeatureToggleListItem) => {
|
|
if (feature?.favorite) {
|
|
await unfavorite(projectId, feature.name);
|
|
} else {
|
|
await favorite(projectId, feature.name);
|
|
}
|
|
refetch();
|
|
},
|
|
[projectId, refetch]
|
|
);
|
|
|
|
const columns = useMemo(
|
|
() => [
|
|
{
|
|
id: 'favorite',
|
|
Header: (
|
|
<FavoriteIconHeader
|
|
isActive={isFavoritesPinned}
|
|
onClick={onChangeIsFavoritePinned}
|
|
/>
|
|
),
|
|
accessor: 'favorite',
|
|
Cell: ({ row: { original: feature } }: any) => (
|
|
<FavoriteIconCell
|
|
value={feature?.favorite}
|
|
onClick={() => onFavorite(feature)}
|
|
/>
|
|
),
|
|
maxWidth: 50,
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
Header: 'Seen',
|
|
accessor: 'lastSeenAt',
|
|
Cell: FeatureSeenCell,
|
|
sortType: 'date',
|
|
align: 'center',
|
|
maxWidth: 80,
|
|
},
|
|
{
|
|
Header: 'Type',
|
|
accessor: 'type',
|
|
Cell: FeatureTypeCell,
|
|
align: 'center',
|
|
maxWidth: 80,
|
|
},
|
|
{
|
|
Header: 'Name',
|
|
accessor: 'name',
|
|
Cell: ({ value }: { value: string }) => (
|
|
<LinkCell
|
|
title={value}
|
|
to={`/projects/${projectId}/features/${value}`}
|
|
/>
|
|
),
|
|
minWidth: 100,
|
|
sortType: 'alphanumeric',
|
|
searchable: true,
|
|
},
|
|
{
|
|
id: 'tags',
|
|
Header: 'Tags',
|
|
accessor: (row: IFeatureToggleListItem) =>
|
|
row.tags
|
|
?.map(({ type, value }) => `${type}:${value}`)
|
|
.join('\n') || '',
|
|
Cell: FeatureTagCell,
|
|
width: 80,
|
|
hideInMenu: true,
|
|
searchable: true,
|
|
},
|
|
{
|
|
Header: 'Created',
|
|
accessor: 'createdAt',
|
|
Cell: DateCell,
|
|
sortType: 'date',
|
|
minWidth: 120,
|
|
},
|
|
...environments.map((name: string) => ({
|
|
Header: loading ? () => '' : name,
|
|
maxWidth: 90,
|
|
id: `environments.${name}`,
|
|
accessor: (row: ListItemType) =>
|
|
row.environments[name]?.enabled,
|
|
align: 'center',
|
|
Cell: ({
|
|
value,
|
|
row: { original: feature },
|
|
}: {
|
|
value: boolean;
|
|
row: { original: ListItemType };
|
|
}) => {
|
|
const hasWarning =
|
|
feature.someEnabledEnvironmentHasVariants &&
|
|
feature.environments[name].variantCount === 0 &&
|
|
feature.environments[name].enabled;
|
|
|
|
return (
|
|
<StyledSwitchContainer hasWarning={hasWarning}>
|
|
<FeatureToggleSwitch
|
|
value={value}
|
|
projectId={projectId}
|
|
featureName={feature?.name}
|
|
environmentName={name}
|
|
onToggle={onToggle}
|
|
/>
|
|
<ConditionallyRender
|
|
condition={hasWarning}
|
|
show={<VariantsWarningTooltip />}
|
|
/>
|
|
</StyledSwitchContainer>
|
|
);
|
|
},
|
|
sortType: 'boolean',
|
|
filterName: name,
|
|
filterParsing: (value: boolean) =>
|
|
value ? 'enabled' : 'disabled',
|
|
})),
|
|
{
|
|
id: 'Actions',
|
|
maxWidth: 56,
|
|
width: 56,
|
|
Cell: (props: { row: { original: ListItemType } }) => (
|
|
<ActionsCell
|
|
projectId={projectId}
|
|
onOpenArchiveDialog={setFeatureArchiveState}
|
|
onOpenStaleDialog={setFeatureStaleDialogState}
|
|
{...props}
|
|
/>
|
|
),
|
|
disableSortBy: true,
|
|
},
|
|
],
|
|
[projectId, environments, loading, onToggle]
|
|
);
|
|
|
|
const [searchValue, setSearchValue] = useState(
|
|
searchParams.get('search') || ''
|
|
);
|
|
|
|
const featuresData = useMemo(
|
|
() =>
|
|
features.map(feature => ({
|
|
...feature,
|
|
environments: Object.fromEntries(
|
|
environments.map(env => {
|
|
const thisEnv = feature?.environments.find(
|
|
featureEnvironment =>
|
|
featureEnvironment?.name === env
|
|
);
|
|
return [
|
|
env,
|
|
{
|
|
name: env,
|
|
enabled: thisEnv?.enabled || false,
|
|
variantCount: thisEnv?.variantCount || 0,
|
|
},
|
|
];
|
|
})
|
|
),
|
|
someEnabledEnvironmentHasVariants:
|
|
feature.environments?.some(
|
|
featureEnvironment =>
|
|
featureEnvironment.variantCount > 0 &&
|
|
featureEnvironment.enabled
|
|
) || false,
|
|
})),
|
|
[features, environments]
|
|
);
|
|
|
|
const {
|
|
data: searchedData,
|
|
getSearchText,
|
|
getSearchContext,
|
|
} = useSearch(columns, searchValue, featuresData);
|
|
|
|
const data = useMemo(() => {
|
|
if (loading) {
|
|
return Array(6).fill({
|
|
type: '-',
|
|
name: 'Feature name',
|
|
createdAt: new Date(),
|
|
environments: {
|
|
production: { name: 'production', enabled: false },
|
|
},
|
|
}) as object[];
|
|
}
|
|
return searchedData;
|
|
}, [loading, searchedData]);
|
|
|
|
const initialState = useMemo(
|
|
() => {
|
|
const allColumnIds = columns.map(
|
|
(column: any) => column?.accessor || column?.id
|
|
);
|
|
let hiddenColumns = environments
|
|
.filter((_, index) => index >= 3)
|
|
.map(environment => `environments.${environment}`);
|
|
|
|
if (searchParams.has('columns')) {
|
|
const columnsInParams =
|
|
searchParams.get('columns')?.split(',') || [];
|
|
const visibleColumns = [...staticColumns, ...columnsInParams];
|
|
hiddenColumns = allColumnIds.filter(
|
|
columnId => !visibleColumns.includes(columnId)
|
|
);
|
|
} else if (storedParams.columns) {
|
|
const visibleColumns = [
|
|
...staticColumns,
|
|
...storedParams.columns,
|
|
];
|
|
hiddenColumns = allColumnIds.filter(
|
|
columnId => !visibleColumns.includes(columnId)
|
|
);
|
|
}
|
|
|
|
return {
|
|
sortBy: [
|
|
{
|
|
id: searchParams.get('sort') || 'createdAt',
|
|
desc: searchParams.has('order')
|
|
? searchParams.get('order') === 'desc'
|
|
: storedParams.desc,
|
|
},
|
|
],
|
|
hiddenColumns,
|
|
};
|
|
},
|
|
[environments] // eslint-disable-line react-hooks/exhaustive-deps
|
|
);
|
|
|
|
const getRowId = useCallback((row: any) => row.name, []);
|
|
|
|
const {
|
|
allColumns,
|
|
headerGroups,
|
|
rows,
|
|
state: { sortBy, hiddenColumns },
|
|
prepareRow,
|
|
setHiddenColumns,
|
|
} = useTable(
|
|
{
|
|
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
|
data,
|
|
initialState,
|
|
sortTypes,
|
|
autoResetHiddenColumns: false,
|
|
disableSortRemove: true,
|
|
autoResetSortBy: false,
|
|
getRowId,
|
|
},
|
|
useFlexLayout,
|
|
useSortBy
|
|
);
|
|
|
|
useConditionallyHiddenColumns(
|
|
[
|
|
{
|
|
condition: !features.some(({ tags }) => tags?.length),
|
|
columns: ['tags'],
|
|
},
|
|
],
|
|
setHiddenColumns,
|
|
columns
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (loading) {
|
|
return;
|
|
}
|
|
const tableState: Record<string, string> = {};
|
|
tableState.sort = sortBy[0].id;
|
|
if (sortBy[0].desc) {
|
|
tableState.order = 'desc';
|
|
}
|
|
if (searchValue) {
|
|
tableState.search = searchValue;
|
|
}
|
|
if (isFavoritesPinned) {
|
|
tableState.favorites = 'true';
|
|
}
|
|
tableState.columns = allColumns
|
|
.map(({ id }) => id)
|
|
.filter(
|
|
id =>
|
|
!staticColumns.includes(id) && !hiddenColumns?.includes(id)
|
|
)
|
|
.join(',');
|
|
|
|
setSearchParams(tableState, {
|
|
replace: true,
|
|
});
|
|
setStoredParams(params => ({
|
|
...params,
|
|
id: sortBy[0].id,
|
|
desc: sortBy[0].desc || false,
|
|
columns: tableState.columns.split(','),
|
|
}));
|
|
setGlobalStore(params => ({
|
|
...params,
|
|
favorites: Boolean(isFavoritesPinned),
|
|
}));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [
|
|
loading,
|
|
sortBy,
|
|
hiddenColumns,
|
|
searchValue,
|
|
setSearchParams,
|
|
isFavoritesPinned,
|
|
]);
|
|
|
|
return (
|
|
<PageContent
|
|
isLoading={loading}
|
|
className={styles.container}
|
|
header={
|
|
<PageHeader
|
|
titleElement={`Feature toggles (${rows.length})`}
|
|
actions={
|
|
<>
|
|
<ConditionallyRender
|
|
condition={!isSmallScreen}
|
|
show={
|
|
<Search
|
|
initialValue={searchValue}
|
|
onChange={setSearchValue}
|
|
hasFilters
|
|
getSearchContext={getSearchContext}
|
|
/>
|
|
}
|
|
/>
|
|
<ColumnsMenu
|
|
allColumns={allColumns}
|
|
staticColumns={staticColumns}
|
|
dividerAfter={['createdAt']}
|
|
dividerBefore={['Actions']}
|
|
isCustomized={Boolean(storedParams.columns)}
|
|
setHiddenColumns={setHiddenColumns}
|
|
/>
|
|
<PageHeader.Divider sx={{ marginLeft: 0 }} />
|
|
<ConditionallyRender
|
|
condition={Boolean(
|
|
uiConfig?.flags?.featuresExportImport
|
|
)}
|
|
show={
|
|
<Tooltip
|
|
title="Export current selection"
|
|
arrow
|
|
>
|
|
<IconButton
|
|
onClick={() =>
|
|
setShowExportDialog(true)
|
|
}
|
|
sx={theme => ({
|
|
marginRight: theme.spacing(2),
|
|
})}
|
|
>
|
|
<FileDownload />
|
|
</IconButton>
|
|
</Tooltip>
|
|
}
|
|
/>
|
|
<StyledResponsiveButton
|
|
onClick={() =>
|
|
navigate(getCreateTogglePath(projectId))
|
|
}
|
|
maxWidth="960px"
|
|
Icon={Add}
|
|
projectId={projectId}
|
|
permission={CREATE_FEATURE}
|
|
data-testid="NAVIGATE_TO_CREATE_FEATURE"
|
|
>
|
|
New feature toggle
|
|
</StyledResponsiveButton>
|
|
</>
|
|
}
|
|
>
|
|
<ConditionallyRender
|
|
condition={isSmallScreen}
|
|
show={
|
|
<Search
|
|
initialValue={searchValue}
|
|
onChange={setSearchValue}
|
|
hasFilters
|
|
getSearchContext={getSearchContext}
|
|
/>
|
|
}
|
|
/>
|
|
</PageHeader>
|
|
}
|
|
>
|
|
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
|
<VirtualizedTable
|
|
rows={rows}
|
|
headerGroups={headerGroups}
|
|
prepareRow={prepareRow}
|
|
/>
|
|
</SearchHighlightProvider>
|
|
<ConditionallyRender
|
|
condition={rows.length === 0}
|
|
show={
|
|
<ConditionallyRender
|
|
condition={searchValue?.length > 0}
|
|
show={
|
|
<TablePlaceholder>
|
|
No feature toggles found matching “
|
|
{searchValue}
|
|
”
|
|
</TablePlaceholder>
|
|
}
|
|
elseShow={
|
|
<TablePlaceholder>
|
|
No feature toggles available. Get started by
|
|
adding a new feature toggle.
|
|
</TablePlaceholder>
|
|
}
|
|
/>
|
|
}
|
|
/>
|
|
<EnvironmentStrategyDialog
|
|
onClose={() =>
|
|
setStrategiesDialogState(prev => ({ ...prev, open: false }))
|
|
}
|
|
projectId={projectId}
|
|
{...strategiesDialogState}
|
|
/>
|
|
<FeatureStaleDialog
|
|
isStale={featureStaleDialogState.stale === true}
|
|
isOpen={Boolean(featureStaleDialogState.featureId)}
|
|
onClose={() => {
|
|
setFeatureStaleDialogState({});
|
|
refetch();
|
|
}}
|
|
featureId={featureStaleDialogState.featureId || ''}
|
|
projectId={projectId}
|
|
/>
|
|
<FeatureArchiveDialog
|
|
isOpen={Boolean(featureArchiveState)}
|
|
onConfirm={() => {
|
|
refetch();
|
|
}}
|
|
onClose={() => {
|
|
setFeatureArchiveState(undefined);
|
|
}}
|
|
featureId={featureArchiveState || ''}
|
|
projectId={projectId}
|
|
/>{' '}
|
|
<ChangeRequestDialogue
|
|
isOpen={changeRequestDialogDetails.isOpen}
|
|
onClose={onChangeRequestToggleClose}
|
|
environment={changeRequestDialogDetails?.environment}
|
|
onConfirm={onChangeRequestToggleConfirm}
|
|
messageComponent={
|
|
<UpdateEnabledMessage
|
|
featureName={changeRequestDialogDetails.featureName!}
|
|
enabled={changeRequestDialogDetails.enabled!}
|
|
environment={changeRequestDialogDetails?.environment!}
|
|
/>
|
|
}
|
|
/>
|
|
<ConditionallyRender
|
|
condition={
|
|
Boolean(uiConfig?.flags?.featuresExportImport) && !loading
|
|
}
|
|
show={
|
|
<ExportDialog
|
|
showExportDialog={showExportDialog}
|
|
data={data}
|
|
onClose={() => setShowExportDialog(false)}
|
|
environments={environments}
|
|
/>
|
|
}
|
|
/>
|
|
</PageContent>
|
|
);
|
|
};
|