mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-27 11:02:16 +01:00
Refactor flag filters (#10703)
Refactored and simplified code around flag filters, in preparation for UI improvements. It's split into 2 PRs in order to simplify what needs to be behind a flag and what doesn't. - `ExperimentalColumnsMenu` moved to `ColumnsMenu`, old unused `ColumnsMenu` removed - Parts of the code moved to `ProjectFeaturesColumnsMenu` - Moved `FlagCreationButton` to a separate file - Removed part behind archived flag (`projectOverviewRefactorFeedback`)
This commit is contained in:
parent
e46f8881d1
commit
c12aca72db
@ -4,7 +4,7 @@ import {
|
|||||||
UPDATE_PROJECT,
|
UPDATE_PROJECT,
|
||||||
CREATE_PROJECT_API_TOKEN,
|
CREATE_PROJECT_API_TOKEN,
|
||||||
} from 'component/providers/AccessProvider/permissions';
|
} from 'component/providers/AccessProvider/permissions';
|
||||||
import { FlagCreationButton } from '../../project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx';
|
import { FlagCreationButton } from '../../project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/FlagCreationButton/FlagCreationButton.tsx';
|
||||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
import { SdkExample } from './SdkExample.tsx';
|
import { SdkExample } from './SdkExample.tsx';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, type VFC } from 'react';
|
import { useState, type FC } from 'react';
|
||||||
import {
|
import {
|
||||||
IconButton,
|
IconButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
@ -17,7 +17,7 @@ import {
|
|||||||
StyledDivider,
|
StyledDivider,
|
||||||
StyledIconButton,
|
StyledIconButton,
|
||||||
StyledMenuItem,
|
StyledMenuItem,
|
||||||
} from './ExperimentalColumnsMenu.styles';
|
} from './ColumnsMenu.styles';
|
||||||
|
|
||||||
interface IColumnsMenuProps {
|
interface IColumnsMenuProps {
|
||||||
columns: {
|
columns: {
|
||||||
@ -29,10 +29,7 @@ interface IColumnsMenuProps {
|
|||||||
onToggle?: (id: string) => void;
|
onToggle?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExperimentalColumnsMenu: VFC<IColumnsMenuProps> = ({
|
export const ColumnsMenu: FC<IColumnsMenuProps> = ({ columns, onToggle }) => {
|
||||||
columns,
|
|
||||||
onToggle,
|
|
||||||
}) => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
const onIconClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const onIconClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
Checkbox,
|
|
||||||
Divider,
|
|
||||||
IconButton,
|
|
||||||
MenuItem,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material';
|
|
||||||
|
|
||||||
import { flexRow } from 'themes/themeStyles';
|
|
||||||
|
|
||||||
export const StyledBoxContainer = styled(Box)(() => ({
|
|
||||||
...flexRow,
|
|
||||||
justifyContent: 'center',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledIconButton = styled(IconButton)(({ theme }) => ({
|
|
||||||
margin: theme.spacing(-1, 0),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledBoxMenuHeader = styled(Box)(({ theme }) => ({
|
|
||||||
...flexRow,
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
padding: theme.spacing(1, 1, 0, 4),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledMenuItem = styled(MenuItem)(({ theme }) => ({
|
|
||||||
padding: theme.spacing(0, 2),
|
|
||||||
margin: theme.spacing(0, 2),
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledDivider = styled(Divider)(({ theme }) => ({
|
|
||||||
'&.MuiDivider-root.MuiDivider-fullWidth': {
|
|
||||||
margin: theme.spacing(0.75, 0),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledCheckbox = styled(Checkbox)(({ theme }) => ({
|
|
||||||
padding: theme.spacing(0.75, 1),
|
|
||||||
}));
|
|
||||||
@ -9,7 +9,6 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC
|
|||||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||||
import { ActionsCell } from '../ProjectFeatureToggles/ActionsCell/ActionsCell.tsx';
|
import { ActionsCell } from '../ProjectFeatureToggles/ActionsCell/ActionsCell.tsx';
|
||||||
import { ExperimentalColumnsMenu as ColumnsMenu } from './ExperimentalColumnsMenu/ExperimentalColumnsMenu.tsx';
|
|
||||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||||
import { MemoizedRowSelectCell } from '../ProjectFeatureToggles/RowSelectCell/RowSelectCell.tsx';
|
import { MemoizedRowSelectCell } from '../ProjectFeatureToggles/RowSelectCell/RowSelectCell.tsx';
|
||||||
import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||||
@ -56,14 +55,13 @@ import { UPDATE_FEATURE } from '@server/types/permissions';
|
|||||||
import { ImportModal } from '../Import/ImportModal.tsx';
|
import { ImportModal } from '../Import/ImportModal.tsx';
|
||||||
import { IMPORT_BUTTON } from 'utils/testIds';
|
import { IMPORT_BUTTON } from 'utils/testIds';
|
||||||
import { ProjectCleanupReminder } from './ProjectCleanupReminder/ProjectCleanupReminder.tsx';
|
import { ProjectCleanupReminder } from './ProjectCleanupReminder/ProjectCleanupReminder.tsx';
|
||||||
|
import { formatEnvironmentColumnId } from './formatEnvironmentColumnId.ts';
|
||||||
|
import { ProjectFeaturesColumnsMenu } from './ProjectFeaturesColumnsMenu/ProjectFeaturesColumnsMenu.tsx';
|
||||||
|
|
||||||
type ProjectFeatureTogglesProps = {
|
type ProjectFeatureTogglesProps = {
|
||||||
environments: string[];
|
environments: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatEnvironmentColumnId = (environment: string) =>
|
|
||||||
`environment:${environment}`;
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
||||||
const getRowId = (row: { name: string }) => row.name;
|
const getRowId = (row: { name: string }) => row.name;
|
||||||
|
|
||||||
@ -514,50 +512,9 @@ export const ProjectFeatureToggles = ({
|
|||||||
dataToExport={data}
|
dataToExport={data}
|
||||||
environmentsToExport={environments}
|
environmentsToExport={environments}
|
||||||
actions={
|
actions={
|
||||||
<ColumnsMenu
|
<ProjectFeaturesColumnsMenu
|
||||||
columns={[
|
columnVisibility={columnVisibility}
|
||||||
{
|
environments={environments}
|
||||||
header: 'Name',
|
|
||||||
id: 'name',
|
|
||||||
isVisible: columnVisibility.name,
|
|
||||||
isStatic: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Created',
|
|
||||||
id: 'createdAt',
|
|
||||||
isVisible: columnVisibility.createdAt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'By',
|
|
||||||
id: 'createdBy',
|
|
||||||
isVisible: columnVisibility.createdBy,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Last seen',
|
|
||||||
id: 'lastSeenAt',
|
|
||||||
isVisible: columnVisibility.lastSeenAt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Lifecycle',
|
|
||||||
id: 'lifecycle',
|
|
||||||
isVisible: columnVisibility.lifecycle,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'divider',
|
|
||||||
},
|
|
||||||
...environments.map((environment) => ({
|
|
||||||
header: environment,
|
|
||||||
id: formatEnvironmentColumnId(
|
|
||||||
environment,
|
|
||||||
),
|
|
||||||
isVisible:
|
|
||||||
columnVisibility[
|
|
||||||
formatEnvironmentColumnId(
|
|
||||||
environment,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
]}
|
|
||||||
onToggle={onToggleColumnVisibility}
|
onToggle={onToggleColumnVisibility}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import Add from '@mui/icons-material/Add';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { CreateFeatureDialog } from '../CreateFeatureDialog.tsx';
|
||||||
|
import type { OverridableStringUnion } from '@mui/types';
|
||||||
|
import type { ButtonPropsVariantOverrides } from '@mui/material/Button/Button';
|
||||||
|
import { NAVIGATE_TO_CREATE_FEATURE } from 'utils/testIds';
|
||||||
|
|
||||||
|
interface IFlagCreationButtonProps {
|
||||||
|
text?: string;
|
||||||
|
variant?: OverridableStringUnion<
|
||||||
|
'text' | 'outlined' | 'contained',
|
||||||
|
ButtonPropsVariantOverrides
|
||||||
|
>;
|
||||||
|
skipNavigationOnComplete?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FlagCreationButton = ({
|
||||||
|
variant,
|
||||||
|
text = 'New feature flag',
|
||||||
|
skipNavigationOnComplete,
|
||||||
|
isLoading,
|
||||||
|
onSuccess,
|
||||||
|
}: IFlagCreationButtonProps) => {
|
||||||
|
const { loading } = useUiConfig();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const showCreateDialog = Boolean(searchParams.get('create'));
|
||||||
|
const [openCreateDialog, setOpenCreateDialog] = useState(showCreateDialog);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledResponsiveButton
|
||||||
|
onClick={() => setOpenCreateDialog(true)}
|
||||||
|
maxWidth='960px'
|
||||||
|
Icon={Add}
|
||||||
|
projectId={projectId}
|
||||||
|
disabled={loading || isLoading}
|
||||||
|
variant={variant}
|
||||||
|
permission={CREATE_FEATURE}
|
||||||
|
data-testid={
|
||||||
|
loading || isLoading ? '' : NAVIGATE_TO_CREATE_FEATURE
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</StyledResponsiveButton>
|
||||||
|
<CreateFeatureDialog
|
||||||
|
open={openCreateDialog}
|
||||||
|
onClose={() => setOpenCreateDialog(false)}
|
||||||
|
skipNavigationOnComplete={skipNavigationOnComplete}
|
||||||
|
onSuccess={onSuccess}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { type FC, useState } from 'react';
|
||||||
|
import { ReactComponent as ImportSvg } from 'assets/icons/import.svg';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
|
import { UPDATE_FEATURE } from '@server/types/permissions';
|
||||||
|
|
||||||
|
import { ImportModal } from '../../../Import/ImportModal.tsx';
|
||||||
|
import { IMPORT_BUTTON } from 'utils/testIds';
|
||||||
|
|
||||||
|
type ImportButtonProps = {};
|
||||||
|
|
||||||
|
export const ImportButton: FC<ImportButtonProps> = () => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PermissionIconButton
|
||||||
|
permission={UPDATE_FEATURE}
|
||||||
|
projectId={projectId}
|
||||||
|
onClick={() => setModalOpen(true)}
|
||||||
|
tooltipProps={{ title: 'Import' }}
|
||||||
|
data-testid={IMPORT_BUTTON}
|
||||||
|
data-loading-project
|
||||||
|
>
|
||||||
|
<ImportSvg />
|
||||||
|
</PermissionIconButton>
|
||||||
|
<ImportModal
|
||||||
|
open={modalOpen}
|
||||||
|
setOpen={setModalOpen}
|
||||||
|
project={projectId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { type ReactNode, type FC, useState } from 'react';
|
import { type ReactNode, type FC, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
@ -11,24 +10,12 @@ 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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
|
||||||
import Add from '@mui/icons-material/Add';
|
|
||||||
import { styled } from '@mui/material';
|
|
||||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||||
import type { FeatureSchema } from 'openapi';
|
import type { FeatureSchema } from 'openapi';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import ReviewsOutlined from '@mui/icons-material/ReviewsOutlined';
|
|
||||||
import { useFeedback } from 'component/feedbackNew/useFeedback';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { CreateFeatureDialog } from './CreateFeatureDialog.tsx';
|
|
||||||
import IosShare from '@mui/icons-material/IosShare';
|
import IosShare from '@mui/icons-material/IosShare';
|
||||||
import type { OverridableStringUnion } from '@mui/types';
|
import { FlagCreationButton } from './FlagCreationButton/FlagCreationButton.tsx';
|
||||||
import type { ButtonPropsVariantOverrides } from '@mui/material/Button/Button';
|
|
||||||
import { NAVIGATE_TO_CREATE_FEATURE } from 'utils/testIds';
|
|
||||||
|
|
||||||
interface IProjectFeatureTogglesHeaderProps {
|
interface IProjectFeatureTogglesHeaderProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
@ -40,60 +27,6 @@ interface IProjectFeatureTogglesHeaderProps {
|
|||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IFlagCreationButtonProps {
|
|
||||||
text?: string;
|
|
||||||
variant?: OverridableStringUnion<
|
|
||||||
'text' | 'outlined' | 'contained',
|
|
||||||
ButtonPropsVariantOverrides
|
|
||||||
>;
|
|
||||||
skipNavigationOnComplete?: boolean;
|
|
||||||
isLoading?: boolean;
|
|
||||||
onSuccess?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const FlagCreationButton = ({
|
|
||||||
variant,
|
|
||||||
text = 'New feature flag',
|
|
||||||
skipNavigationOnComplete,
|
|
||||||
isLoading,
|
|
||||||
onSuccess,
|
|
||||||
}: IFlagCreationButtonProps) => {
|
|
||||||
const { loading } = useUiConfig();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const projectId = useRequiredPathParam('projectId');
|
|
||||||
const showCreateDialog = Boolean(searchParams.get('create'));
|
|
||||||
const [openCreateDialog, setOpenCreateDialog] = useState(showCreateDialog);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StyledResponsiveButton
|
|
||||||
onClick={() => setOpenCreateDialog(true)}
|
|
||||||
maxWidth='960px'
|
|
||||||
Icon={Add}
|
|
||||||
projectId={projectId}
|
|
||||||
disabled={loading || isLoading}
|
|
||||||
variant={variant}
|
|
||||||
permission={CREATE_FEATURE}
|
|
||||||
data-testid={
|
|
||||||
loading || isLoading ? '' : NAVIGATE_TO_CREATE_FEATURE
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</StyledResponsiveButton>
|
|
||||||
<CreateFeatureDialog
|
|
||||||
open={openCreateDialog}
|
|
||||||
onClose={() => setOpenCreateDialog(false)}
|
|
||||||
skipNavigationOnComplete={skipNavigationOnComplete}
|
|
||||||
onSuccess={onSuccess}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectFeatureTogglesHeader: FC<
|
export const ProjectFeatureTogglesHeader: FC<
|
||||||
IProjectFeatureTogglesHeaderProps
|
IProjectFeatureTogglesHeaderProps
|
||||||
> = ({
|
> = ({
|
||||||
@ -111,10 +44,6 @@ export const ProjectFeatureTogglesHeader: FC<
|
|||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
const projectOverviewRefactorFeedback = useUiFlag(
|
|
||||||
'projectOverviewRefactorFeedback',
|
|
||||||
);
|
|
||||||
const { openFeedback } = useFeedback('newProjectOverview', 'automatic');
|
|
||||||
const handleSearch = (query: string) => {
|
const handleSearch = (query: string) => {
|
||||||
onChangeSearchQuery?.(query);
|
onChangeSearchQuery?.(query);
|
||||||
trackEvent('search-bar', {
|
trackEvent('search-bar', {
|
||||||
@ -125,16 +54,6 @@ export const ProjectFeatureTogglesHeader: FC<
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={headerLoadingRef}
|
ref={headerLoadingRef}
|
||||||
@ -196,22 +115,6 @@ export const ProjectFeatureTogglesHeader: FC<
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
|
||||||
condition={
|
|
||||||
projectOverviewRefactorFeedback &&
|
|
||||||
!isSmallScreen
|
|
||||||
}
|
|
||||||
show={
|
|
||||||
<Button
|
|
||||||
startIcon={<ReviewsOutlined />}
|
|
||||||
onClick={createFeedbackContext}
|
|
||||||
variant='outlined'
|
|
||||||
data-loading
|
|
||||||
>
|
|
||||||
Provide feedback
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FlagCreationButton isLoading={isLoading} />
|
<FlagCreationButton isLoading={isLoading} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { ColumnsMenu } from '../ColumnsMenu/ColumnsMenu.tsx';
|
||||||
|
import { formatEnvironmentColumnId } from '../formatEnvironmentColumnId.ts';
|
||||||
|
|
||||||
|
type ProjectFeaturesColumnsMenuProps = {
|
||||||
|
columnVisibility: Record<string, boolean>;
|
||||||
|
environments: string[];
|
||||||
|
onToggle: (id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectFeaturesColumnsMenu: FC<
|
||||||
|
ProjectFeaturesColumnsMenuProps
|
||||||
|
> = ({ columnVisibility, environments, onToggle }) => {
|
||||||
|
return (
|
||||||
|
<ColumnsMenu
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'Name',
|
||||||
|
id: 'name',
|
||||||
|
isVisible: columnVisibility.name,
|
||||||
|
isStatic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Created',
|
||||||
|
id: 'createdAt',
|
||||||
|
isVisible: columnVisibility.createdAt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'By',
|
||||||
|
id: 'createdBy',
|
||||||
|
isVisible: columnVisibility.createdBy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Last seen',
|
||||||
|
id: 'lastSeenAt',
|
||||||
|
isVisible: columnVisibility.lastSeenAt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Lifecycle',
|
||||||
|
id: 'lifecycle',
|
||||||
|
isVisible: columnVisibility.lifecycle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'divider',
|
||||||
|
},
|
||||||
|
...environments.map((environment) => ({
|
||||||
|
header: environment,
|
||||||
|
id: formatEnvironmentColumnId(environment),
|
||||||
|
isVisible:
|
||||||
|
columnVisibility[
|
||||||
|
formatEnvironmentColumnId(environment)
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
onToggle={onToggle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import useLoading from 'hooks/useLoading';
|
||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
|
||||||
|
interface IProjectFlagsSearchProps {
|
||||||
|
isLoading?: boolean;
|
||||||
|
searchQuery?: string;
|
||||||
|
onChangeSearchQuery?: (query: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectFlagsSearch: FC<IProjectFlagsSearchProps> = ({
|
||||||
|
isLoading,
|
||||||
|
searchQuery,
|
||||||
|
onChangeSearchQuery,
|
||||||
|
}) => {
|
||||||
|
const headerLoadingRef = useLoading(isLoading || false);
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
const handleSearch = (query: string) => {
|
||||||
|
onChangeSearchQuery?.(query);
|
||||||
|
trackEvent('search-bar', {
|
||||||
|
props: {
|
||||||
|
screen: 'project',
|
||||||
|
length: query.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box ref={headerLoadingRef} aria-busy={isLoading} aria-live='polite'>
|
||||||
|
<Search
|
||||||
|
placeholder='Search'
|
||||||
|
expandable
|
||||||
|
initialValue={searchQuery || ''}
|
||||||
|
onChange={handleSearch}
|
||||||
|
hasFilters
|
||||||
|
id='projectFeatureFlags'
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export const formatEnvironmentColumnId = (environment: string) =>
|
||||||
|
`environment:${environment}`;
|
||||||
@ -1,212 +0,0 @@
|
|||||||
import { useEffect, useState, type VFC } from 'react';
|
|
||||||
import {
|
|
||||||
IconButton,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
MenuList,
|
|
||||||
Popover,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
useMediaQuery,
|
|
||||||
useTheme,
|
|
||||||
} from '@mui/material';
|
|
||||||
import ColumnIcon from '@mui/icons-material/ViewWeek';
|
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import {
|
|
||||||
StyledBoxContainer,
|
|
||||||
StyledBoxMenuHeader,
|
|
||||||
StyledCheckbox,
|
|
||||||
StyledDivider,
|
|
||||||
StyledIconButton,
|
|
||||||
StyledMenuItem,
|
|
||||||
} from './ColumnsMenu.styles';
|
|
||||||
|
|
||||||
interface IColumnsMenuProps {
|
|
||||||
allColumns: {
|
|
||||||
Header?: string | any;
|
|
||||||
id: string;
|
|
||||||
isVisible: boolean;
|
|
||||||
toggleHidden: (state: boolean) => void;
|
|
||||||
hideInMenu?: boolean;
|
|
||||||
}[];
|
|
||||||
staticColumns?: string[];
|
|
||||||
dividerBefore?: string[];
|
|
||||||
dividerAfter?: string[];
|
|
||||||
isCustomized?: boolean;
|
|
||||||
setHiddenColumns: (hiddenColumns: string[]) => void;
|
|
||||||
onCustomize?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnNameMap: Record<string, string> = {
|
|
||||||
favorite: 'Favorite',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
|
||||||
allColumns,
|
|
||||||
staticColumns = [],
|
|
||||||
dividerBefore = [],
|
|
||||||
dividerAfter = [],
|
|
||||||
isCustomized = false,
|
|
||||||
onCustomize,
|
|
||||||
setHiddenColumns,
|
|
||||||
}) => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
||||||
const theme = useTheme();
|
|
||||||
const isTinyScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isCustomized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const setVisibleColumns = (
|
|
||||||
columns: string[],
|
|
||||||
environmentsToShow: number = 0,
|
|
||||||
) => {
|
|
||||||
const visibleEnvColumns = allColumns
|
|
||||||
.filter(({ id }) => id.startsWith('environment:') !== false)
|
|
||||||
.map(({ id }) => id)
|
|
||||||
.slice(0, environmentsToShow);
|
|
||||||
const hiddenColumns = allColumns
|
|
||||||
.map(({ id }) => id)
|
|
||||||
.filter((id) => !columns.includes(id))
|
|
||||||
.filter((id) => !staticColumns.includes(id))
|
|
||||||
.filter((id) => !visibleEnvColumns.includes(id));
|
|
||||||
setHiddenColumns(hiddenColumns);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isTinyScreen) {
|
|
||||||
return setVisibleColumns(['createdAt']);
|
|
||||||
}
|
|
||||||
if (isSmallScreen) {
|
|
||||||
return setVisibleColumns(['createdAt'], 1);
|
|
||||||
}
|
|
||||||
if (isMediumScreen) {
|
|
||||||
return setVisibleColumns(['type', 'createdAt'], 1);
|
|
||||||
}
|
|
||||||
setVisibleColumns(['lastSeenAt', 'type', 'createdAt'], 3);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isTinyScreen, isSmallScreen, isMediumScreen]);
|
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isOpen = Boolean(anchorEl);
|
|
||||||
const id = `columns-menu`;
|
|
||||||
const menuId = `columns-menu-list-${id}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledBoxContainer>
|
|
||||||
<Tooltip title='Select columns' arrow describeChild>
|
|
||||||
<StyledIconButton
|
|
||||||
id={id}
|
|
||||||
aria-controls={isOpen ? menuId : undefined}
|
|
||||||
aria-haspopup='true'
|
|
||||||
aria-expanded={isOpen ? 'true' : undefined}
|
|
||||||
onClick={handleClick}
|
|
||||||
type='button'
|
|
||||||
size='large'
|
|
||||||
data-loading
|
|
||||||
>
|
|
||||||
<ColumnIcon />
|
|
||||||
</StyledIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
id={menuId}
|
|
||||||
open={isOpen}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
onClose={handleClose}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
disableScrollLock={true}
|
|
||||||
PaperProps={{
|
|
||||||
sx: (theme) => ({
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
paddingBottom: theme.spacing(2),
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledBoxMenuHeader>
|
|
||||||
<Typography variant='body2'>
|
|
||||||
<strong>Columns</strong>
|
|
||||||
</Typography>
|
|
||||||
<IconButton onClick={handleClose}>
|
|
||||||
<CloseIcon />
|
|
||||||
</IconButton>
|
|
||||||
</StyledBoxMenuHeader>
|
|
||||||
<MenuList>
|
|
||||||
{allColumns
|
|
||||||
.filter(({ hideInMenu }) => !hideInMenu)
|
|
||||||
.map((column) => [
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={dividerBefore.includes(column.id)}
|
|
||||||
show={<StyledDivider />}
|
|
||||||
/>,
|
|
||||||
<StyledMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
column.toggleHidden(column.isVisible);
|
|
||||||
onCustomize?.();
|
|
||||||
}}
|
|
||||||
disabled={staticColumns.includes(column.id)}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<StyledCheckbox
|
|
||||||
edge='start'
|
|
||||||
checked={column.isVisible}
|
|
||||||
disableRipple
|
|
||||||
inputProps={{
|
|
||||||
'aria-labelledby': column.id,
|
|
||||||
}}
|
|
||||||
size='medium'
|
|
||||||
/>
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText
|
|
||||||
id={column.id}
|
|
||||||
primary={
|
|
||||||
<Typography variant='body2'>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(
|
|
||||||
typeof column.Header ===
|
|
||||||
'string' &&
|
|
||||||
column.Header,
|
|
||||||
)}
|
|
||||||
show={() => (
|
|
||||||
<>{column.Header}</>
|
|
||||||
)}
|
|
||||||
elseShow={() => (
|
|
||||||
<>
|
|
||||||
{columnNameMap[
|
|
||||||
column.id
|
|
||||||
] || column.id}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledMenuItem>,
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={dividerAfter.includes(column.id)}
|
|
||||||
show={<StyledDivider />}
|
|
||||||
/>,
|
|
||||||
])}
|
|
||||||
</MenuList>
|
|
||||||
</Popover>
|
|
||||||
</StyledBoxContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -74,7 +74,6 @@ export type UiFlags = {
|
|||||||
outdatedSdksBanner?: boolean;
|
outdatedSdksBanner?: boolean;
|
||||||
estimateTrafficDataCost?: boolean;
|
estimateTrafficDataCost?: boolean;
|
||||||
disableShowContextFieldSelectionValues?: boolean;
|
disableShowContextFieldSelectionValues?: boolean;
|
||||||
projectOverviewRefactorFeedback?: boolean;
|
|
||||||
featureLifecycle?: boolean;
|
featureLifecycle?: boolean;
|
||||||
manyStrategiesPagination?: boolean;
|
manyStrategiesPagination?: boolean;
|
||||||
enableLegacyVariants?: boolean;
|
enableLegacyVariants?: boolean;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user