mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-27 11:02:16 +01:00
feat: improve flag filters on project page (#10705)
This commit is contained in:
parent
c12aca72db
commit
9d996f14d9
@ -37,7 +37,6 @@ interface ILifecycleFiltersBaseProps {
|
|||||||
const Wrapper = styled(Box)(({ theme }) => ({
|
const Wrapper = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
minHeight: theme.spacing(7),
|
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
import { useFeatureToggleSwitch } from '../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch.tsx';
|
import { useFeatureToggleSwitch } from '../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch.tsx';
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx';
|
import { ProjectFeatureTogglesHeader as LegacyProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/LegacyProjectFeatureTogglesHeader.tsx';
|
||||||
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
||||||
import { withTableState } from 'utils/withTableState';
|
import { withTableState } from 'utils/withTableState';
|
||||||
import type { FeatureSearchResponseSchema } from 'openapi';
|
import type { FeatureSearchResponseSchema } from 'openapi';
|
||||||
@ -41,7 +41,7 @@ import {
|
|||||||
useProjectFeatureSearchActions,
|
useProjectFeatureSearchActions,
|
||||||
} from './useProjectFeatureSearch.ts';
|
} from './useProjectFeatureSearch.ts';
|
||||||
import { AvatarCell } from './AvatarCell.tsx';
|
import { AvatarCell } from './AvatarCell.tsx';
|
||||||
import { styled } from '@mui/material';
|
import { styled, useMediaQuery, useTheme } from '@mui/material';
|
||||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
import { ConnectSdkDialog } from '../../../onboarding/dialog/ConnectSdkDialog.tsx';
|
import { ConnectSdkDialog } from '../../../onboarding/dialog/ConnectSdkDialog.tsx';
|
||||||
import { ProjectOnboarding } from '../../../onboarding/flow/ProjectOnboarding.tsx';
|
import { ProjectOnboarding } from '../../../onboarding/flow/ProjectOnboarding.tsx';
|
||||||
@ -57,6 +57,9 @@ import { IMPORT_BUTTON } from 'utils/testIds';
|
|||||||
import { ProjectCleanupReminder } from './ProjectCleanupReminder/ProjectCleanupReminder.tsx';
|
import { ProjectCleanupReminder } from './ProjectCleanupReminder/ProjectCleanupReminder.tsx';
|
||||||
import { formatEnvironmentColumnId } from './formatEnvironmentColumnId.ts';
|
import { formatEnvironmentColumnId } from './formatEnvironmentColumnId.ts';
|
||||||
import { ProjectFeaturesColumnsMenu } from './ProjectFeaturesColumnsMenu/ProjectFeaturesColumnsMenu.tsx';
|
import { ProjectFeaturesColumnsMenu } from './ProjectFeaturesColumnsMenu/ProjectFeaturesColumnsMenu.tsx';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag.ts';
|
||||||
|
import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx';
|
||||||
|
import { ProjectFlagsSearch } from './ProjectFlagsSearch/ProjectFlagsSearch.tsx';
|
||||||
|
|
||||||
type ProjectFeatureTogglesProps = {
|
type ProjectFeatureTogglesProps = {
|
||||||
environments: string[];
|
environments: string[];
|
||||||
@ -71,12 +74,27 @@ const Container = styled('div')(({ theme }) => ({
|
|||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const FilterRow = styled('div')(({ theme }) => ({
|
const LegacyFilterRow = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexFlow: 'row wrap',
|
flexFlow: 'row wrap',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const FiltersContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
padding: theme.spacing(2, 3, 2),
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
padding: theme.spacing(2, 2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const FilterRow = styled('div')({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
const ButtonGroup = styled('div')(({ theme }) => ({
|
const ButtonGroup = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
@ -91,6 +109,9 @@ export const ProjectFeatureToggles = ({
|
|||||||
const { project } = useProjectOverview(projectId);
|
const { project } = useProjectOverview(projectId);
|
||||||
const [connectSdkOpen, setConnectSdkOpen] = useState(false);
|
const [connectSdkOpen, setConnectSdkOpen] = useState(false);
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const flagsUiFilterRefactorEnabled = useUiFlag('flagsUiFilterRefactor');
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
features,
|
features,
|
||||||
@ -500,25 +521,32 @@ export const ProjectFeatureToggles = ({
|
|||||||
) : null}
|
) : null}
|
||||||
<PageContent
|
<PageContent
|
||||||
disableLoading
|
disableLoading
|
||||||
disablePadding
|
|
||||||
header={
|
header={
|
||||||
<ProjectFeatureTogglesHeader
|
flagsUiFilterRefactorEnabled ? (
|
||||||
isLoading={initialLoad}
|
<ProjectFeatureTogglesHeader
|
||||||
totalItems={total}
|
isLoading={initialLoad}
|
||||||
searchQuery={tableState.query || ''}
|
totalItems={total}
|
||||||
onChangeSearchQuery={(query) => {
|
environmentsToExport={environments}
|
||||||
setTableState({ query });
|
/>
|
||||||
}}
|
) : (
|
||||||
dataToExport={data}
|
<LegacyProjectFeatureTogglesHeader
|
||||||
environmentsToExport={environments}
|
isLoading={initialLoad}
|
||||||
actions={
|
totalItems={total}
|
||||||
<ProjectFeaturesColumnsMenu
|
searchQuery={tableState.query || ''}
|
||||||
columnVisibility={columnVisibility}
|
onChangeSearchQuery={(query) => {
|
||||||
environments={environments}
|
setTableState({ query });
|
||||||
onToggle={onToggleColumnVisibility}
|
}}
|
||||||
/>
|
dataToExport={data}
|
||||||
}
|
environmentsToExport={environments}
|
||||||
/>
|
actions={
|
||||||
|
<ProjectFeaturesColumnsMenu
|
||||||
|
columnVisibility={columnVisibility}
|
||||||
|
environments={environments}
|
||||||
|
onToggle={onToggleColumnVisibility}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
bodyClass='noop'
|
bodyClass='noop'
|
||||||
style={{ cursor: 'inherit' }}
|
style={{ cursor: 'inherit' }}
|
||||||
@ -528,31 +556,74 @@ export const ProjectFeatureToggles = ({
|
|||||||
aria-busy={isPlaceholder}
|
aria-busy={isPlaceholder}
|
||||||
aria-live='polite'
|
aria-live='polite'
|
||||||
>
|
>
|
||||||
<FilterRow>
|
{flagsUiFilterRefactorEnabled ? (
|
||||||
<ProjectOverviewFilters
|
<FiltersContainer>
|
||||||
project={projectId}
|
<FilterRow>
|
||||||
onChange={setTableState}
|
<ProjectLifecycleFilters
|
||||||
state={filterState}
|
projectId={projectId}
|
||||||
/>
|
state={filterState}
|
||||||
<ProjectLifecycleFilters
|
onChange={setTableState}
|
||||||
projectId={projectId}
|
total={loading ? undefined : total}
|
||||||
state={filterState}
|
/>
|
||||||
onChange={setTableState}
|
{isSmallScreen ? null : (
|
||||||
total={loading ? undefined : total}
|
<ProjectFlagsSearch
|
||||||
/>
|
searchQuery={tableState.query || ''}
|
||||||
<ButtonGroup>
|
onChangeSearchQuery={(query) => {
|
||||||
<PermissionIconButton
|
setTableState({ query });
|
||||||
permission={UPDATE_FEATURE}
|
}}
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ProjectFeaturesColumnsMenu
|
||||||
|
columnVisibility={columnVisibility}
|
||||||
|
environments={environments}
|
||||||
|
onToggle={onToggleColumnVisibility}
|
||||||
|
/>
|
||||||
|
</FilterRow>
|
||||||
|
<FilterRow>
|
||||||
|
<ProjectOverviewFilters
|
||||||
|
project={projectId}
|
||||||
|
onChange={setTableState}
|
||||||
|
state={filterState}
|
||||||
|
/>
|
||||||
|
</FilterRow>
|
||||||
|
{isSmallScreen ? (
|
||||||
|
<ProjectFlagsSearch
|
||||||
|
searchQuery={tableState.query || ''}
|
||||||
|
onChangeSearchQuery={(query) => {
|
||||||
|
setTableState({ query });
|
||||||
|
}}
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</FiltersContainer>
|
||||||
|
) : (
|
||||||
|
<LegacyFilterRow>
|
||||||
|
<ProjectOverviewFilters
|
||||||
|
project={projectId}
|
||||||
|
onChange={setTableState}
|
||||||
|
state={filterState}
|
||||||
|
/>
|
||||||
|
<ProjectLifecycleFilters
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
onClick={() => setModalOpen(true)}
|
state={filterState}
|
||||||
tooltipProps={{ title: 'Import' }}
|
onChange={setTableState}
|
||||||
data-testid={IMPORT_BUTTON}
|
total={loading ? undefined : total}
|
||||||
data-loading-project
|
/>
|
||||||
>
|
<ButtonGroup>
|
||||||
<ImportSvg />
|
<PermissionIconButton
|
||||||
</PermissionIconButton>
|
permission={UPDATE_FEATURE}
|
||||||
</ButtonGroup>
|
projectId={projectId}
|
||||||
</FilterRow>
|
onClick={() => setModalOpen(true)}
|
||||||
|
tooltipProps={{ title: 'Import' }}
|
||||||
|
data-testid={IMPORT_BUTTON}
|
||||||
|
data-loading-project
|
||||||
|
>
|
||||||
|
<ImportSvg />
|
||||||
|
</PermissionIconButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</LegacyFilterRow>
|
||||||
|
)}
|
||||||
<SearchHighlightProvider value={tableState.query || ''}>
|
<SearchHighlightProvider value={tableState.query || ''}>
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
tableInstance={table}
|
tableInstance={table}
|
||||||
|
|||||||
@ -0,0 +1,165 @@
|
|||||||
|
import { type ReactNode, type FC, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import useLoading from 'hooks/useLoading';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||||
|
import type { FeatureSchema } from 'openapi';
|
||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import ReviewsOutlined from '@mui/icons-material/ReviewsOutlined';
|
||||||
|
import { useFeedback } from 'component/feedbackNew/useFeedback';
|
||||||
|
import IosShare from '@mui/icons-material/IosShare';
|
||||||
|
import { FlagCreationButton } from './FlagCreationButton/FlagCreationButton.tsx';
|
||||||
|
|
||||||
|
interface IProjectFeatureTogglesHeaderProps {
|
||||||
|
isLoading?: boolean;
|
||||||
|
totalItems?: number;
|
||||||
|
searchQuery?: string;
|
||||||
|
onChangeSearchQuery?: (query: string) => void;
|
||||||
|
dataToExport?: Pick<FeatureSchema, 'name'>[];
|
||||||
|
environmentsToExport?: string[];
|
||||||
|
actions?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated remove with `flagsUiFilterRefactor` flag
|
||||||
|
*/
|
||||||
|
export const ProjectFeatureTogglesHeader: FC<
|
||||||
|
IProjectFeatureTogglesHeaderProps
|
||||||
|
> = ({
|
||||||
|
isLoading,
|
||||||
|
totalItems,
|
||||||
|
searchQuery,
|
||||||
|
onChangeSearchQuery,
|
||||||
|
environmentsToExport,
|
||||||
|
actions,
|
||||||
|
}) => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const headerLoadingRef = useLoading(isLoading || false);
|
||||||
|
const [showTitle, setShowTitle] = useState(true);
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
const projectOverviewRefactorFeedback = false;
|
||||||
|
const { openFeedback } = useFeedback('newProjectOverview', 'automatic');
|
||||||
|
const handleSearch = (query: string) => {
|
||||||
|
onChangeSearchQuery?.(query);
|
||||||
|
trackEvent('search-bar', {
|
||||||
|
props: {
|
||||||
|
screen: 'project',
|
||||||
|
length: query.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFeedbackContext = () => {
|
||||||
|
openFeedback({
|
||||||
|
title: 'How easy was it to work with the project overview in Unleash?',
|
||||||
|
positiveLabel:
|
||||||
|
'What do you like most about the updated project overview?',
|
||||||
|
areasForImprovementsLabel:
|
||||||
|
'What improvements are needed in the project overview?',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box ref={headerLoadingRef} aria-busy={isLoading} aria-live='polite'>
|
||||||
|
<PageHeader
|
||||||
|
titleElement={
|
||||||
|
showTitle
|
||||||
|
? `Feature flags ${
|
||||||
|
totalItems !== undefined ? `(${totalItems})` : ''
|
||||||
|
}`
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
data-loading
|
||||||
|
placeholder='Search and Filter'
|
||||||
|
expandable
|
||||||
|
initialValue={searchQuery || ''}
|
||||||
|
onChange={handleSearch}
|
||||||
|
onFocus={() => setShowTitle(false)}
|
||||||
|
onBlur={() => setShowTitle(true)}
|
||||||
|
hasFilters
|
||||||
|
id='projectFeatureFlags'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{actions}
|
||||||
|
<PageHeader.Divider sx={{ marginLeft: 0 }} />
|
||||||
|
<Tooltip title='Export all project flags' arrow>
|
||||||
|
<IconButton
|
||||||
|
data-loading
|
||||||
|
onClick={() => setShowExportDialog(true)}
|
||||||
|
sx={(theme) => ({
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<IosShare />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!isLoading}
|
||||||
|
show={
|
||||||
|
<ExportDialog
|
||||||
|
showExportDialog={showExportDialog}
|
||||||
|
project={projectId}
|
||||||
|
data={[]}
|
||||||
|
onClose={() => setShowExportDialog(false)}
|
||||||
|
environments={environmentsToExport || []}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* FIXME: remove */}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={
|
||||||
|
projectOverviewRefactorFeedback &&
|
||||||
|
!isSmallScreen
|
||||||
|
}
|
||||||
|
show={
|
||||||
|
<Button
|
||||||
|
startIcon={<ReviewsOutlined />}
|
||||||
|
onClick={createFeedbackContext}
|
||||||
|
variant='outlined'
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
Provide feedback
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FlagCreationButton isLoading={isLoading} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
initialValue={searchQuery || ''}
|
||||||
|
onChange={handleSearch}
|
||||||
|
hasFilters
|
||||||
|
id='projectFeatureFlags'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageHeader>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,136 +1,59 @@
|
|||||||
import { type ReactNode, type FC, useState } from 'react';
|
import { type FC, useState } from 'react';
|
||||||
import {
|
import { Box, IconButton, Tooltip } from '@mui/material';
|
||||||
Box,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
useMediaQuery,
|
|
||||||
useTheme,
|
|
||||||
} from '@mui/material';
|
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { Search } from 'component/common/Search/Search';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||||
import type { FeatureSchema } from 'openapi';
|
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
|
||||||
import IosShare from '@mui/icons-material/IosShare';
|
import IosShare from '@mui/icons-material/IosShare';
|
||||||
import { FlagCreationButton } from './FlagCreationButton/FlagCreationButton.tsx';
|
import { FlagCreationButton } from './FlagCreationButton/FlagCreationButton.tsx';
|
||||||
|
import { ImportButton } from './ImportButton/ImportButton.tsx';
|
||||||
|
|
||||||
interface IProjectFeatureTogglesHeaderProps {
|
type ProjectFeatureTogglesHeaderProps = {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
totalItems?: number;
|
totalItems?: number;
|
||||||
searchQuery?: string;
|
|
||||||
onChangeSearchQuery?: (query: string) => void;
|
|
||||||
dataToExport?: Pick<FeatureSchema, 'name'>[];
|
|
||||||
environmentsToExport?: string[];
|
environmentsToExport?: string[];
|
||||||
actions?: ReactNode;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ProjectFeatureTogglesHeader: FC<
|
export const ProjectFeatureTogglesHeader: FC<
|
||||||
IProjectFeatureTogglesHeaderProps
|
ProjectFeatureTogglesHeaderProps
|
||||||
> = ({
|
> = ({ isLoading, totalItems, environmentsToExport }) => {
|
||||||
isLoading,
|
|
||||||
totalItems,
|
|
||||||
searchQuery,
|
|
||||||
onChangeSearchQuery,
|
|
||||||
environmentsToExport,
|
|
||||||
actions,
|
|
||||||
}) => {
|
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const headerLoadingRef = useLoading(isLoading || false);
|
const headerLoadingRef = useLoading(isLoading || false);
|
||||||
const [showTitle, setShowTitle] = useState(true);
|
|
||||||
const theme = useTheme();
|
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
const { trackEvent } = usePlausibleTracker();
|
|
||||||
const handleSearch = (query: string) => {
|
|
||||||
onChangeSearchQuery?.(query);
|
|
||||||
trackEvent('search-bar', {
|
|
||||||
props: {
|
|
||||||
screen: 'project',
|
|
||||||
length: query.length,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box ref={headerLoadingRef} aria-busy={isLoading} aria-live='polite'>
|
||||||
ref={headerLoadingRef}
|
|
||||||
aria-busy={isLoading}
|
|
||||||
aria-live='polite'
|
|
||||||
sx={(theme) => ({
|
|
||||||
padding: `${theme.spacing(2.5)} ${theme.spacing(3.125)}`,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
titleElement={
|
titleElement={`Feature flags ${
|
||||||
showTitle
|
totalItems !== undefined ? `(${totalItems})` : ''
|
||||||
? `Feature flags ${
|
}`}
|
||||||
totalItems !== undefined ? `(${totalItems})` : ''
|
|
||||||
}`
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
|
||||||
condition={!isSmallScreen}
|
|
||||||
show={
|
|
||||||
<Search
|
|
||||||
data-loading
|
|
||||||
placeholder='Search and Filter'
|
|
||||||
expandable
|
|
||||||
initialValue={searchQuery || ''}
|
|
||||||
onChange={handleSearch}
|
|
||||||
onFocus={() => setShowTitle(false)}
|
|
||||||
onBlur={() => setShowTitle(true)}
|
|
||||||
hasFilters
|
|
||||||
id='projectFeatureFlags'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{actions}
|
|
||||||
<PageHeader.Divider sx={{ marginLeft: 0 }} />
|
|
||||||
<Tooltip title='Export all project flags' arrow>
|
<Tooltip title='Export all project flags' arrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
data-loading
|
data-loading
|
||||||
onClick={() => setShowExportDialog(true)}
|
onClick={() => setShowExportDialog(true)}
|
||||||
sx={(theme) => ({
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<IosShare />
|
<IosShare />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<ImportButton />
|
||||||
|
|
||||||
<ConditionallyRender
|
{!isLoading ? (
|
||||||
condition={!isLoading}
|
<ExportDialog
|
||||||
show={
|
showExportDialog={showExportDialog}
|
||||||
<ExportDialog
|
project={projectId}
|
||||||
showExportDialog={showExportDialog}
|
data={[]}
|
||||||
project={projectId}
|
onClose={() => setShowExportDialog(false)}
|
||||||
data={[]}
|
environments={environmentsToExport || []}
|
||||||
onClose={() => setShowExportDialog(false)}
|
/>
|
||||||
environments={environmentsToExport || []}
|
) : null}
|
||||||
/>
|
<Box>
|
||||||
}
|
<FlagCreationButton isLoading={isLoading} />
|
||||||
/>
|
</Box>
|
||||||
<FlagCreationButton isLoading={isLoading} />
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<ConditionallyRender
|
|
||||||
condition={isSmallScreen}
|
|
||||||
show={
|
|
||||||
<Search
|
|
||||||
initialValue={searchQuery || ''}
|
|
||||||
onChange={handleSearch}
|
|
||||||
hasFilters
|
|
||||||
id='projectFeatureFlags'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PageHeader>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
import { ColumnsMenu } from '../ColumnsMenu/ColumnsMenu.tsx';
|
import { ColumnsMenu } from '../ColumnsMenu/ColumnsMenu.tsx';
|
||||||
import { formatEnvironmentColumnId } from '../formatEnvironmentColumnId.ts';
|
import { formatEnvironmentColumnId } from '../formatEnvironmentColumnId.ts';
|
||||||
|
|
||||||
@ -12,47 +13,49 @@ export const ProjectFeaturesColumnsMenu: FC<
|
|||||||
ProjectFeaturesColumnsMenuProps
|
ProjectFeaturesColumnsMenuProps
|
||||||
> = ({ columnVisibility, environments, onToggle }) => {
|
> = ({ columnVisibility, environments, onToggle }) => {
|
||||||
return (
|
return (
|
||||||
<ColumnsMenu
|
<Box sx={(theme) => ({ marginLeft: theme.spacing(1) })}>
|
||||||
columns={[
|
<ColumnsMenu
|
||||||
{
|
columns={[
|
||||||
header: 'Name',
|
{
|
||||||
id: 'name',
|
header: 'Name',
|
||||||
isVisible: columnVisibility.name,
|
id: 'name',
|
||||||
isStatic: true,
|
isVisible: columnVisibility.name,
|
||||||
},
|
isStatic: true,
|
||||||
{
|
},
|
||||||
header: 'Created',
|
{
|
||||||
id: 'createdAt',
|
header: 'Created',
|
||||||
isVisible: columnVisibility.createdAt,
|
id: 'createdAt',
|
||||||
},
|
isVisible: columnVisibility.createdAt,
|
||||||
{
|
},
|
||||||
header: 'By',
|
{
|
||||||
id: 'createdBy',
|
header: 'By',
|
||||||
isVisible: columnVisibility.createdBy,
|
id: 'createdBy',
|
||||||
},
|
isVisible: columnVisibility.createdBy,
|
||||||
{
|
},
|
||||||
header: 'Last seen',
|
{
|
||||||
id: 'lastSeenAt',
|
header: 'Last seen',
|
||||||
isVisible: columnVisibility.lastSeenAt,
|
id: 'lastSeenAt',
|
||||||
},
|
isVisible: columnVisibility.lastSeenAt,
|
||||||
{
|
},
|
||||||
header: 'Lifecycle',
|
{
|
||||||
id: 'lifecycle',
|
header: 'Lifecycle',
|
||||||
isVisible: columnVisibility.lifecycle,
|
id: 'lifecycle',
|
||||||
},
|
isVisible: columnVisibility.lifecycle,
|
||||||
{
|
},
|
||||||
id: 'divider',
|
{
|
||||||
},
|
id: 'divider',
|
||||||
...environments.map((environment) => ({
|
},
|
||||||
header: environment,
|
...environments.map((environment) => ({
|
||||||
id: formatEnvironmentColumnId(environment),
|
header: environment,
|
||||||
isVisible:
|
id: formatEnvironmentColumnId(environment),
|
||||||
columnVisibility[
|
isVisible:
|
||||||
formatEnvironmentColumnId(environment)
|
columnVisibility[
|
||||||
],
|
formatEnvironmentColumnId(environment)
|
||||||
})),
|
],
|
||||||
]}
|
})),
|
||||||
onToggle={onToggle}
|
]}
|
||||||
/>
|
onToggle={onToggle}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { FilterItemParamHolder } from '../../../filter/Filters/Filters.tsx'
|
|||||||
import { useProjectStatus } from 'hooks/api/getters/useProjectStatus/useProjectStatus';
|
import { useProjectStatus } from 'hooks/api/getters/useProjectStatus/useProjectStatus';
|
||||||
import { LifecycleFilters } from 'component/common/LifecycleFilters/LifecycleFilters.tsx';
|
import { LifecycleFilters } from 'component/common/LifecycleFilters/LifecycleFilters.tsx';
|
||||||
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag.ts';
|
||||||
|
|
||||||
type ProjectLifecycleFiltersProps = {
|
type ProjectLifecycleFiltersProps = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -23,6 +24,7 @@ export const ProjectLifecycleFilters: FC<ProjectLifecycleFiltersProps> = ({
|
|||||||
const { data } = useProjectStatus(projectId);
|
const { data } = useProjectStatus(projectId);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const flagsUiFilterRefactorEnabled = useUiFlag('flagsUiFilterRefactor');
|
||||||
const lifecycleSummary = Object.entries(
|
const lifecycleSummary = Object.entries(
|
||||||
data?.lifecycleSummary || {},
|
data?.lifecycleSummary || {},
|
||||||
).reduce(
|
).reduce(
|
||||||
@ -48,7 +50,13 @@ export const ProjectLifecycleFilters: FC<ProjectLifecycleFiltersProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
margin: isSmallScreen ? theme.spacing(0, 2) : '0 auto 0 0',
|
...(!flagsUiFilterRefactorEnabled
|
||||||
|
? {
|
||||||
|
margin: isSmallScreen
|
||||||
|
? theme.spacing(0, 3)
|
||||||
|
: `${theme.spacing(1.5)} auto 0 0`,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LifecycleFilters
|
<LifecycleFilters
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import {
|
|||||||
} from 'component/filter/Filters/Filters';
|
} from 'component/filter/Filters/Filters';
|
||||||
import { useProjectFlagCreators } from 'hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators';
|
import { useProjectFlagCreators } from 'hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators';
|
||||||
import { formatTag } from 'utils/format-tag';
|
import { formatTag } from 'utils/format-tag';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
type ProjectOverviewFiltersProps = {
|
type ProjectOverviewFiltersProps = {
|
||||||
state: FilterItemParamHolder;
|
state: FilterItemParamHolder;
|
||||||
@ -14,6 +16,10 @@ type ProjectOverviewFiltersProps = {
|
|||||||
project: string;
|
project: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledFilters = styled(Filters)({
|
||||||
|
padding: 0,
|
||||||
|
});
|
||||||
|
|
||||||
export const ProjectOverviewFilters: FC<ProjectOverviewFiltersProps> = ({
|
export const ProjectOverviewFilters: FC<ProjectOverviewFiltersProps> = ({
|
||||||
state,
|
state,
|
||||||
onChange,
|
onChange,
|
||||||
@ -22,6 +28,10 @@ export const ProjectOverviewFilters: FC<ProjectOverviewFiltersProps> = ({
|
|||||||
const { tags } = useAllTags();
|
const { tags } = useAllTags();
|
||||||
const { flagCreators } = useProjectFlagCreators(project);
|
const { flagCreators } = useProjectFlagCreators(project);
|
||||||
const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]);
|
const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]);
|
||||||
|
const flagsUiFilterRefactorEnabled = useUiFlag('flagsUiFilterRefactor');
|
||||||
|
const FilterComponent = flagsUiFilterRefactorEnabled
|
||||||
|
? StyledFilters
|
||||||
|
: Filters;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tagsOptions = (tags || []).map((tag) => {
|
const tagsOptions = (tags || []).map((tag) => {
|
||||||
@ -124,7 +134,7 @@ export const ProjectOverviewFilters: FC<ProjectOverviewFiltersProps> = ({
|
|||||||
}, [JSON.stringify(tags), JSON.stringify(flagCreators)]);
|
}, [JSON.stringify(tags), JSON.stringify(flagCreators)]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Filters
|
<FilterComponent
|
||||||
availableFilters={availableFilters}
|
availableFilters={availableFilters}
|
||||||
state={state}
|
state={state}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|||||||
@ -88,6 +88,7 @@ export type UiFlags = {
|
|||||||
lifecycleGraphs?: boolean;
|
lifecycleGraphs?: boolean;
|
||||||
newStrategyModal?: boolean;
|
newStrategyModal?: boolean;
|
||||||
globalChangeRequestList?: boolean;
|
globalChangeRequestList?: boolean;
|
||||||
|
flagsUiFilterRefactor?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user