1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

UI/bulk select (#3267)

Select multiple toggles on project overview.
This commit is contained in:
Tymoteusz Czech 2023-03-14 09:56:03 +01:00 committed by GitHub
parent cb75a68cc3
commit 30a753b93f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 406 additions and 213 deletions

View File

@ -23,7 +23,13 @@ import {
IPersonalAPIToken, IPersonalAPIToken,
} from 'interfaces/personalAPIToken'; } from 'interfaces/personalAPIToken';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useTable, SortingRule, useSortBy, useFlexLayout } from 'react-table'; import {
useTable,
SortingRule,
useSortBy,
useFlexLayout,
Column,
} from 'react-table';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { ServiceAccountCreateTokenDialog } from './ServiceAccountCreateTokenDialog/ServiceAccountCreateTokenDialog'; import { ServiceAccountCreateTokenDialog } from './ServiceAccountCreateTokenDialog/ServiceAccountCreateTokenDialog';
import { ServiceAccountTokenDialog } from 'component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountTokenDialog/ServiceAccountTokenDialog'; import { ServiceAccountTokenDialog } from 'component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountTokenDialog/ServiceAccountTokenDialog';
@ -151,65 +157,69 @@ export const ServiceAccountTokens = ({
}; };
const columns = useMemo( const columns = useMemo(
() => [ () =>
{ [
Header: 'Description', {
accessor: 'description', Header: 'Description',
Cell: HighlightCell, accessor: 'description',
minWidth: 100, Cell: HighlightCell,
searchable: true, minWidth: 100,
}, searchable: true,
{
Header: 'Expires',
accessor: 'expiresAt',
Cell: ({ value }: { value: string }) => {
const date = new Date(value);
if (date.getFullYear() > new Date().getFullYear() + 100) {
return <TextCell>Never</TextCell>;
}
return <DateCell value={value} />;
}, },
sortType: 'date', {
maxWidth: 150, Header: 'Expires',
}, accessor: 'expiresAt',
{ Cell: ({ value }: { value: string }) => {
Header: 'Created', const date = new Date(value);
accessor: 'createdAt', if (
Cell: DateCell, date.getFullYear() >
sortType: 'date', new Date().getFullYear() + 100
maxWidth: 150, ) {
}, return <TextCell>Never</TextCell>;
{ }
Header: 'Last seen', return <DateCell value={value} />;
accessor: 'seenAt', },
Cell: TimeAgoCell, sortType: 'date',
sortType: 'date', maxWidth: 150,
maxWidth: 150, },
}, {
{ Header: 'Created',
Header: 'Actions', accessor: 'createdAt',
id: 'Actions', Cell: DateCell,
align: 'center', sortType: 'date',
Cell: ({ row: { original: rowToken } }: any) => ( maxWidth: 150,
<ActionCell> },
<Tooltip title="Delete token" arrow describeChild> {
<span> Header: 'Last seen',
<IconButton accessor: 'seenAt',
onClick={() => { Cell: TimeAgoCell,
setSelectedToken(rowToken); sortType: 'date',
setDeleteOpen(true); maxWidth: 150,
}} },
> {
<Delete /> Header: 'Actions',
</IconButton> id: 'Actions',
</span> align: 'center',
</Tooltip> Cell: ({ row: { original: rowToken } }: any) => (
</ActionCell> <ActionCell>
), <Tooltip title="Delete token" arrow describeChild>
maxWidth: 100, <span>
disableSortBy: true, <IconButton
}, onClick={() => {
], setSelectedToken(rowToken);
setDeleteOpen(true);
}}
>
<Delete />
</IconButton>
</span>
</Tooltip>
</ActionCell>
),
maxWidth: 100,
disableSortBy: true,
},
] as Column<IPersonalAPIToken>[],
[setSelectedToken, setDeleteOpen] [setSelectedToken, setDeleteOpen]
); );

View File

@ -1,23 +1,20 @@
import { VFC } from 'react';
import { TableHead, TableRow } from '@mui/material'; import { TableHead, TableRow } from '@mui/material';
import { HeaderGroup } from 'react-table'; import { HeaderGroup } from 'react-table';
import { CellSortable } from './CellSortable/CellSortable'; import { CellSortable } from './CellSortable/CellSortable';
interface ISortableTableHeaderProps { export const SortableTableHeader = <T extends object>({
headerGroups: HeaderGroup<object>[];
className?: string;
flex?: boolean;
}
export const SortableTableHeader: VFC<ISortableTableHeaderProps> = ({
headerGroups, headerGroups,
className, className,
flex, flex,
}: {
headerGroups: HeaderGroup<T>[];
className?: string;
flex?: boolean;
}) => ( }) => (
<TableHead className={className}> <TableHead className={className}>
{headerGroups.map(headerGroup => ( {headerGroups.map(headerGroup => (
<TableRow {...headerGroup.getHeaderGroupProps()}> <TableRow {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column: HeaderGroup) => { {headerGroup.headers.map((column: HeaderGroup<T>) => {
const content = column.render('Header'); const content = column.render('Header');
return ( return (

View File

@ -1,4 +1,4 @@
import { useMemo, VFC } from 'react'; import { useMemo } from 'react';
import { useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import { import {
SortableTableHeader, SortableTableHeader,
@ -10,13 +10,6 @@ import {
import { useVirtualizedRange } from 'hooks/useVirtualizedRange'; import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
import { HeaderGroup, Row } from 'react-table'; import { HeaderGroup, Row } from 'react-table';
interface IVirtualizedTableProps {
rowHeight?: number;
headerGroups: HeaderGroup<object>[];
rows: Row<object>[];
prepareRow: (row: Row) => void;
}
/** /**
* READ BEFORE USE * READ BEFORE USE
* *
@ -27,11 +20,16 @@ interface IVirtualizedTableProps {
* Remember to add `useFlexLayout` to `useTable` * Remember to add `useFlexLayout` to `useTable`
* (more at: https://react-table-v7.tanstack.com/docs/api/useFlexLayout) * (more at: https://react-table-v7.tanstack.com/docs/api/useFlexLayout)
*/ */
export const VirtualizedTable: VFC<IVirtualizedTableProps> = ({ export const VirtualizedTable = <T extends object>({
rowHeight: rowHeightOverride, rowHeight: rowHeightOverride,
headerGroups, headerGroups,
rows, rows,
prepareRow, prepareRow,
}: {
rowHeight?: number;
headerGroups: HeaderGroup<T>[];
rows: Row<T>[];
prepareRow: (row: Row<T>) => void;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const rowHeight = useMemo( const rowHeight = useMemo(

View File

@ -1,16 +1,16 @@
import { createRef, useState } from 'react';
import { styled, Typography, Box } from '@mui/material'; import { styled, Typography, Box } from '@mui/material';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { useExportApi } from 'hooks/api/actions/useExportApi/useExportApi'; import { useExportApi } from 'hooks/api/actions/useExportApi/useExportApi';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { FeatureSchema } from 'openapi'; import type { FeatureSchema } from 'openapi';
import { createRef, useEffect, useState } from 'react';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
interface IExportDialogProps { interface IExportDialogProps {
showExportDialog: boolean; showExportDialog: boolean;
data: FeatureSchema[]; data: Pick<FeatureSchema, 'name'>[];
onClose: () => void; onClose: () => void;
environments: string[]; environments: string[];
} }
@ -37,13 +37,6 @@ export const ExportDialog = ({
label: env, label: env,
})); }));
const getPayload = () => {
return {
features: data.map(feature => feature.name),
environment: selected,
};
};
const downloadFile = (json: any) => { const downloadFile = (json: any) => {
const link = document.createElement('a'); const link = document.createElement('a');
ref.current?.appendChild(link); ref.current?.appendChild(link);
@ -65,7 +58,10 @@ export const ExportDialog = ({
const onClick = async () => { const onClick = async () => {
try { try {
const payload = getPayload(); const payload = {
features: data.map(feature => feature.name),
environment: selected,
};
const res = await createExport(payload); const res = await createExport(payload);
const body = await res.json(); const body = await res.json();
downloadFile(body); downloadFile(body);

View File

@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { import {
Checkbox,
IconButton, IconButton,
styled, styled,
Tooltip, Tooltip,
@ -8,7 +9,14 @@ import {
} from '@mui/material'; } from '@mui/material';
import { Add } from '@mui/icons-material'; import { Add } from '@mui/icons-material';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; import {
SortingRule,
useFlexLayout,
useSortBy,
useRowSelect,
useTable,
} from 'react-table';
import type { FeatureSchema } from 'openapi';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
@ -50,12 +58,13 @@ import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage'; import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import { flexRow } from 'themes/themeStyles'; import { flexRow } from 'themes/themeStyles';
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning'; import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
import FileDownload from '@mui/icons-material/FileDownload'; import FileDownload from '@mui/icons-material/FileDownload';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog'; import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
import { RowSelectCell } from './RowSelectCell/RowSelectCell';
import { SelectionActionsBar } from './SelectionActionsBar/SelectionActionsBar';
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({ const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@ -96,7 +105,7 @@ type ListItemType = Pick<
someEnabledEnvironmentHasVariants: boolean; someEnabledEnvironmentHasVariants: boolean;
}; };
const staticColumns = ['Actions', 'name', 'favorite']; const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
const defaultSort: SortingRule<string> & { const defaultSort: SortingRule<string> & {
columns?: string[]; columns?: string[];
@ -223,8 +232,31 @@ export const ProjectFeatureToggles = ({
[projectId, refetch] [projectId, refetch]
); );
const showTagsColumn = useMemo(
() => features.some(feature => feature?.tags?.length),
[features]
);
const columns = useMemo( const columns = useMemo(
() => [ () => [
...(uiConfig?.flags?.bulkOperations
? [
{
id: 'Select',
Header: ({ getToggleAllRowsSelectedProps }: any) => (
<Checkbox {...getToggleAllRowsSelectedProps()} />
),
Cell: ({ row }: any) => (
<RowSelectCell
{...row?.getToggleRowSelectedProps?.()}
/>
),
maxWidth: 50,
disableSortBy: true,
hideInMenu: true,
},
]
: []),
{ {
id: 'favorite', id: 'favorite',
Header: ( Header: (
@ -242,6 +274,7 @@ export const ProjectFeatureToggles = ({
), ),
maxWidth: 50, maxWidth: 50,
disableSortBy: true, disableSortBy: true,
hideInMenu: true,
}, },
{ {
Header: 'Seen', Header: 'Seen',
@ -271,18 +304,21 @@ export const ProjectFeatureToggles = ({
sortType: 'alphanumeric', sortType: 'alphanumeric',
searchable: true, searchable: true,
}, },
{ ...(showTagsColumn
id: 'tags', ? [
Header: 'Tags', {
accessor: (row: IFeatureToggleListItem) => id: 'tags',
row.tags Header: 'Tags',
?.map(({ type, value }) => `${type}:${value}`) accessor: (row: IFeatureToggleListItem) =>
.join('\n') || '', row.tags
Cell: FeatureTagCell, ?.map(({ type, value }) => `${type}:${value}`)
width: 80, .join('\n') || '',
hideInMenu: true, Cell: FeatureTagCell,
searchable: true, width: 80,
}, searchable: true,
},
]
: []),
{ {
Header: 'Created', Header: 'Created',
accessor: 'createdAt', accessor: 'createdAt',
@ -343,6 +379,7 @@ export const ProjectFeatureToggles = ({
/> />
), ),
disableSortBy: true, disableSortBy: true,
hideInMenu: true,
}, },
], ],
[projectId, environments, loading, onToggle] [projectId, environments, loading, onToggle]
@ -397,7 +434,7 @@ export const ProjectFeatureToggles = ({
environments: { environments: {
production: { name: 'production', enabled: false }, production: { name: 'production', enabled: false },
}, },
}) as object[]; }) as FeatureSchema[];
} }
return searchedData; return searchedData;
}, [loading, searchedData]); }, [loading, searchedData]);
@ -438,6 +475,7 @@ export const ProjectFeatureToggles = ({
}, },
], ],
hiddenColumns, hiddenColumns,
selectedRowIds: {},
}; };
}, },
[environments] // eslint-disable-line react-hooks/exhaustive-deps [environments] // eslint-disable-line react-hooks/exhaustive-deps
@ -449,7 +487,7 @@ export const ProjectFeatureToggles = ({
allColumns, allColumns,
headerGroups, headerGroups,
rows, rows,
state: { sortBy, hiddenColumns }, state: { selectedRowIds, sortBy, hiddenColumns },
prepareRow, prepareRow,
setHiddenColumns, setHiddenColumns,
} = useTable( } = useTable(
@ -464,18 +502,8 @@ export const ProjectFeatureToggles = ({
getRowId, getRowId,
}, },
useFlexLayout, useFlexLayout,
useSortBy useSortBy,
); useRowSelect
useConditionallyHiddenColumns(
[
{
condition: !features.some(({ tags }) => tags?.length),
columns: ['tags'],
},
],
setHiddenColumns,
columns
); );
useEffect(() => { useEffect(() => {
@ -559,7 +587,7 @@ export const ProjectFeatureToggles = ({
)} )}
show={ show={
<Tooltip <Tooltip
title="Export current selection" title="Export toggles visible in the table below"
arrow arrow
> >
<IconButton <IconButton
@ -686,6 +714,10 @@ export const ProjectFeatureToggles = ({
/> />
} }
/> />
<SelectionActionsBar
selectedIds={Object.keys(selectedRowIds)}
data={features}
/>
</PageContent> </PageContent>
); );
}; };

View File

@ -0,0 +1,24 @@
import { Box, Checkbox, styled } from '@mui/material';
import { FC } from 'react';
interface IRowSelectCellProps {
onChange: () => void;
checked: boolean;
title: string;
}
const StyledBoxCell = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
paddingLeft: theme.spacing(2),
}));
export const RowSelectCell: FC<IRowSelectCellProps> = ({
onChange,
checked,
title,
}) => (
<StyledBoxCell>
<Checkbox onChange={onChange} title={title} checked={checked} />
</StyledBoxCell>
);

View File

@ -0,0 +1,120 @@
import { useMemo, useState, VFC } from 'react';
import { Box, Button, Paper, styled, Typography } from '@mui/material';
import { Archive, FileDownload, Label, WatchLater } from '@mui/icons-material';
import type { FeatureSchema } from 'openapi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface ISelectionActionsBarProps {
selectedIds: string[];
data: FeatureSchema[];
}
const StyledContainer = styled(Box)(() => ({
display: 'flex',
justifyContent: 'center',
width: '100%',
}));
const StyledBar = styled(Paper)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: theme.spacing(2),
marginLeft: 'auto',
marginRight: 'auto',
padding: theme.spacing(2, 3),
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.secondary.main}`,
borderRadius: theme.shape.borderRadiusLarge,
columnGap: theme.spacing(1),
}));
const StyledCount = styled('span')(({ theme }) => ({
background: theme.palette.secondary.main,
color: theme.palette.background.paper,
padding: theme.spacing(0.5, 1),
borderRadius: theme.shape.borderRadius,
}));
const StyledText = styled(Typography)(({ theme }) => ({
marginRight: theme.spacing(2),
}));
export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({
selectedIds,
data,
}) => {
const { uiConfig } = useUiConfig();
const [showExportDialog, setShowExportDialog] = useState(false);
const selectedData = useMemo(
() => data.filter(d => selectedIds.includes(d.name)),
[data, selectedIds]
);
const environments = useMemo(() => {
const envs = selectedData
.flatMap(d => d.environments)
.map(env => env?.name)
.filter(env => env !== undefined) as string[];
return Array.from(new Set(envs));
}, [selectedData]);
if (selectedIds.length === 0) {
return null;
}
return (
<StyledContainer>
<StyledBar elevation={4}>
<StyledText>
<StyledCount>{selectedIds.length}</StyledCount>
&ensp;selected
</StyledText>
<Button
disabled
startIcon={<Archive />}
variant="outlined"
size="small"
>
Archive
</Button>
<Button
disabled
startIcon={<WatchLater />}
variant="outlined"
size="small"
>
Mark as stale
</Button>
<Button
startIcon={<FileDownload />}
variant="outlined"
size="small"
onClick={() => setShowExportDialog(true)}
>
Export
</Button>
<Button
disabled
startIcon={<Label />}
variant="outlined"
size="small"
>
Tags
</Button>
</StyledBar>
<ConditionallyRender
condition={Boolean(uiConfig?.flags?.featuresExportImport)}
show={
<ExportDialog
showExportDialog={showExportDialog}
data={selectedData}
onClose={() => setShowExportDialog(false)}
environments={environments}
/>
}
/>
</StyledContainer>
);
};

View File

@ -28,7 +28,13 @@ import {
} from 'interfaces/personalAPIToken'; } from 'interfaces/personalAPIToken';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useTable, SortingRule, useSortBy, useFlexLayout } from 'react-table'; import {
useTable,
SortingRule,
useSortBy,
useFlexLayout,
Column,
} from 'react-table';
import { createLocalStorage } from 'utils/createLocalStorage'; import { createLocalStorage } from 'utils/createLocalStorage';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { CreatePersonalAPIToken } from './CreatePersonalAPIToken/CreatePersonalAPIToken'; import { CreatePersonalAPIToken } from './CreatePersonalAPIToken/CreatePersonalAPIToken';
@ -104,65 +110,69 @@ export const PersonalAPITokensTab = () => {
const [selectedToken, setSelectedToken] = useState<IPersonalAPIToken>(); const [selectedToken, setSelectedToken] = useState<IPersonalAPIToken>();
const columns = useMemo( const columns = useMemo(
() => [ () =>
{ [
Header: 'Description', {
accessor: 'description', Header: 'Description',
Cell: HighlightCell, accessor: 'description',
minWidth: 100, Cell: HighlightCell,
searchable: true, minWidth: 100,
}, searchable: true,
{
Header: 'Expires',
accessor: 'expiresAt',
Cell: ({ value }: { value: string }) => {
const date = new Date(value);
if (date.getFullYear() > new Date().getFullYear() + 100) {
return <TextCell>Never</TextCell>;
}
return <DateCell value={value} />;
}, },
sortType: 'date', {
maxWidth: 150, Header: 'Expires',
}, accessor: 'expiresAt',
{ Cell: ({ value }: { value: string }) => {
Header: 'Created', const date = new Date(value);
accessor: 'createdAt', if (
Cell: DateCell, date.getFullYear() >
sortType: 'date', new Date().getFullYear() + 100
maxWidth: 150, ) {
}, return <TextCell>Never</TextCell>;
{ }
Header: 'Last seen', return <DateCell value={value} />;
accessor: 'seenAt', },
Cell: TimeAgoCell, sortType: 'date',
sortType: 'date', maxWidth: 150,
maxWidth: 150, },
}, {
{ Header: 'Created',
Header: 'Actions', accessor: 'createdAt',
id: 'Actions', Cell: DateCell,
align: 'center', sortType: 'date',
Cell: ({ row: { original: rowToken } }: any) => ( maxWidth: 150,
<ActionCell> },
<Tooltip title="Delete token" arrow describeChild> {
<span> Header: 'Last seen',
<IconButton accessor: 'seenAt',
onClick={() => { Cell: TimeAgoCell,
setSelectedToken(rowToken); sortType: 'date',
setDeleteOpen(true); maxWidth: 150,
}} },
> {
<Delete /> Header: 'Actions',
</IconButton> id: 'Actions',
</span> align: 'center',
</Tooltip> Cell: ({ row: { original: rowToken } }: any) => (
</ActionCell> <ActionCell>
), <Tooltip title="Delete token" arrow describeChild>
maxWidth: 100, <span>
disableSortBy: true, <IconButton
}, onClick={() => {
], setSelectedToken(rowToken);
setDeleteOpen(true);
}}
>
<Delete />
</IconButton>
</span>
</Tooltip>
</ActionCell>
),
maxWidth: 100,
disableSortBy: true,
},
] as Column<IPersonalAPIToken>[],
[setSelectedToken, setDeleteOpen] [setSelectedToken, setDeleteOpen]
); );
@ -186,7 +196,7 @@ export const PersonalAPITokensTab = () => {
prepareRow, prepareRow,
state: { sortBy }, state: { sortBy },
setHiddenColumns, setHiddenColumns,
} = useTable( } = useTable<IPersonalAPIToken>(
{ {
columns, columns,
data, data,

View File

@ -1,6 +1,7 @@
import useSWR from 'swr'; import useSWR from 'swr';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import { PatsSchema } from 'openapi';
import { IPersonalAPIToken } from 'interfaces/personalAPIToken'; import { IPersonalAPIToken } from 'interfaces/personalAPIToken';
export interface IUsePersonalAPITokensOutput { export interface IUsePersonalAPITokensOutput {
@ -10,20 +11,15 @@ export interface IUsePersonalAPITokensOutput {
error?: Error; error?: Error;
} }
export const usePersonalAPITokens = ( export const usePersonalAPITokens = (): IUsePersonalAPITokensOutput => {
userId?: number const { data, error, mutate } = useSWR<PatsSchema>(
): IUsePersonalAPITokensOutput => { formatApiPath('api/admin/user/tokens'),
const { data, error, mutate } = useSWR(
formatApiPath(
userId
? `api/admin/user-admin/${userId}/pat`
: 'api/admin/user/tokens'
),
fetcher fetcher
); );
return { return {
tokens: data ? data.pats : undefined, // FIXME: schema issue
tokens: data ? (data.pats as any) : undefined,
loading: !error && !data, loading: !error && !data,
refetchTokens: () => mutate(), refetchTokens: () => mutate(),
error, error,

View File

@ -1,4 +1,5 @@
import { IPersonalAPIToken } from 'interfaces/personalAPIToken'; import { IPersonalAPIToken } from 'interfaces/personalAPIToken';
import { PatsSchema } from 'openapi';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
@ -11,18 +12,21 @@ export interface IUseServiceAccountTokensOutput {
error?: Error; error?: Error;
} }
export const useServiceAccountTokens = (id: number) => { export const useServiceAccountTokens = (
id: number
): IUseServiceAccountTokensOutput => {
const { isEnterprise } = useUiConfig(); const { isEnterprise } = useUiConfig();
const { data, error, mutate } = useConditionalSWR( const { data, error, mutate } = useConditionalSWR<PatsSchema>(
isEnterprise(), isEnterprise(),
{ tokens: [] }, { pats: [] },
formatApiPath(`api/admin/service-account/${id}/token`), formatApiPath(`api/admin/service-account/${id}/token`),
fetcher fetcher
); );
return { return {
tokens: data ? data.pats : undefined, // FIXME: schema issue
tokens: data ? (data.pats as any) : undefined,
loading: !error && !data, loading: !error && !data,
refetchTokens: () => mutate(), refetchTokens: () => mutate(),
error, error,

View File

@ -7,7 +7,7 @@ import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
import useUiConfig from '../useUiConfig/useUiConfig'; import useUiConfig from '../useUiConfig/useUiConfig';
export const useServiceAccounts = () => { export const useServiceAccounts = () => {
const { uiConfig, isEnterprise } = useUiConfig(); const { isEnterprise } = useUiConfig();
const { data, error, mutate } = useConditionalSWR( const { data, error, mutate } = useConditionalSWR(
isEnterprise(), isEnterprise(),

View File

@ -1,29 +1,34 @@
interface IUseSearchOutput { import { useCallback, useMemo } from 'react';
getSearchText: (input: string) => string;
data: any[];
getSearchContext: () => IGetSearchContextOutput;
}
export interface IGetSearchContextOutput { export type IGetSearchContextOutput<T extends any = any> = {
data: any[]; data: T[];
columns: any[]; columns: any[];
searchValue: string; searchValue: string;
} };
export const useSearch = ( type IUseSearchOutput<T extends any> = {
getSearchText: (input: string) => string;
data: T[];
getSearchContext: () => IGetSearchContextOutput<T>;
};
export const useSearch = <T extends any>(
columns: any[], columns: any[],
searchValue: string, searchValue: string,
data: any[] data: T[]
): IUseSearchOutput => { ): IUseSearchOutput<T> => {
const getSearchText = getSearchTextGenerator(columns); const getSearchText = useCallback(
(value: string) => getSearchTextGenerator(columns)(value),
[columns]
);
const getSearchContext = () => { const getSearchContext = useCallback(() => {
return { data, searchValue, columns }; return { data, searchValue, columns };
}; }, [data, searchValue, columns]);
if (!searchValue) return { data, getSearchText, getSearchContext }; const search = useMemo(() => {
if (!searchValue) return data;
const search = () => {
const filteredData = filter(columns, searchValue, data); const filteredData = filter(columns, searchValue, data);
const searchedData = searchInFilteredData( const searchedData = searchInFilteredData(
columns, columns,
@ -32,9 +37,9 @@ export const useSearch = (
); );
return searchedData; return searchedData;
}; }, [columns, searchValue, data, getSearchText]);
return { data: search(), getSearchText, getSearchContext }; return { data: search, getSearchText, getSearchContext };
}; };
export const filter = (columns: any[], searchValue: string, data: any[]) => { export const filter = (columns: any[], searchValue: string, data: any[]) => {
@ -57,10 +62,10 @@ export const filter = (columns: any[], searchValue: string, data: any[]) => {
return filteredDataSet; return filteredDataSet;
}; };
export const searchInFilteredData = ( export const searchInFilteredData = <T extends any>(
columns: any[], columns: any[],
searchValue: string, searchValue: string,
filteredData: any[] filteredData: T[]
) => { ) => {
const searchableColumns = columns.filter( const searchableColumns = columns.filter(
column => column.searchable && column.accessor column => column.searchable && column.accessor

View File

@ -40,6 +40,7 @@ process.nextTick(async () => {
responseTimeWithAppNameKillSwitch: false, responseTimeWithAppNameKillSwitch: false,
featuresExportImport: true, featuresExportImport: true,
newProjectOverview: true, newProjectOverview: true,
bulkOperations: true,
projectStatusApi: true, projectStatusApi: true,
showProjectApiAccess: true, showProjectApiAccess: true,
projectScopedSegments: true, projectScopedSegments: true,