mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
parent
cb75a68cc3
commit
30a753b93f
@ -23,7 +23,13 @@ import {
|
||||
IPersonalAPIToken,
|
||||
} from 'interfaces/personalAPIToken';
|
||||
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 { ServiceAccountCreateTokenDialog } from './ServiceAccountCreateTokenDialog/ServiceAccountCreateTokenDialog';
|
||||
import { ServiceAccountTokenDialog } from 'component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountTokenDialog/ServiceAccountTokenDialog';
|
||||
@ -151,65 +157,69 @@ export const ServiceAccountTokens = ({
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Description',
|
||||
accessor: 'description',
|
||||
Cell: HighlightCell,
|
||||
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} />;
|
||||
() =>
|
||||
[
|
||||
{
|
||||
Header: 'Description',
|
||||
accessor: 'description',
|
||||
Cell: HighlightCell,
|
||||
minWidth: 100,
|
||||
searchable: true,
|
||||
},
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Last seen',
|
||||
accessor: 'seenAt',
|
||||
Cell: TimeAgoCell,
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Actions',
|
||||
id: 'Actions',
|
||||
align: 'center',
|
||||
Cell: ({ row: { original: rowToken } }: any) => (
|
||||
<ActionCell>
|
||||
<Tooltip title="Delete token" arrow describeChild>
|
||||
<span>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setSelectedToken(rowToken);
|
||||
setDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ActionCell>
|
||||
),
|
||||
maxWidth: 100,
|
||||
disableSortBy: 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: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Last seen',
|
||||
accessor: 'seenAt',
|
||||
Cell: TimeAgoCell,
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Actions',
|
||||
id: 'Actions',
|
||||
align: 'center',
|
||||
Cell: ({ row: { original: rowToken } }: any) => (
|
||||
<ActionCell>
|
||||
<Tooltip title="Delete token" arrow describeChild>
|
||||
<span>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setSelectedToken(rowToken);
|
||||
setDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ActionCell>
|
||||
),
|
||||
maxWidth: 100,
|
||||
disableSortBy: true,
|
||||
},
|
||||
] as Column<IPersonalAPIToken>[],
|
||||
[setSelectedToken, setDeleteOpen]
|
||||
);
|
||||
|
||||
|
@ -1,23 +1,20 @@
|
||||
import { VFC } from 'react';
|
||||
import { TableHead, TableRow } from '@mui/material';
|
||||
import { HeaderGroup } from 'react-table';
|
||||
import { CellSortable } from './CellSortable/CellSortable';
|
||||
|
||||
interface ISortableTableHeaderProps {
|
||||
headerGroups: HeaderGroup<object>[];
|
||||
className?: string;
|
||||
flex?: boolean;
|
||||
}
|
||||
|
||||
export const SortableTableHeader: VFC<ISortableTableHeaderProps> = ({
|
||||
export const SortableTableHeader = <T extends object>({
|
||||
headerGroups,
|
||||
className,
|
||||
flex,
|
||||
}: {
|
||||
headerGroups: HeaderGroup<T>[];
|
||||
className?: string;
|
||||
flex?: boolean;
|
||||
}) => (
|
||||
<TableHead className={className}>
|
||||
{headerGroups.map(headerGroup => (
|
||||
<TableRow {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map((column: HeaderGroup) => {
|
||||
{headerGroup.headers.map((column: HeaderGroup<T>) => {
|
||||
const content = column.render('Header');
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useMemo, VFC } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTheme } from '@mui/material';
|
||||
import {
|
||||
SortableTableHeader,
|
||||
@ -10,13 +10,6 @@ import {
|
||||
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
||||
import { HeaderGroup, Row } from 'react-table';
|
||||
|
||||
interface IVirtualizedTableProps {
|
||||
rowHeight?: number;
|
||||
headerGroups: HeaderGroup<object>[];
|
||||
rows: Row<object>[];
|
||||
prepareRow: (row: Row) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* READ BEFORE USE
|
||||
*
|
||||
@ -27,11 +20,16 @@ interface IVirtualizedTableProps {
|
||||
* Remember to add `useFlexLayout` to `useTable`
|
||||
* (more at: https://react-table-v7.tanstack.com/docs/api/useFlexLayout)
|
||||
*/
|
||||
export const VirtualizedTable: VFC<IVirtualizedTableProps> = ({
|
||||
export const VirtualizedTable = <T extends object>({
|
||||
rowHeight: rowHeightOverride,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
}: {
|
||||
rowHeight?: number;
|
||||
headerGroups: HeaderGroup<T>[];
|
||||
rows: Row<T>[];
|
||||
prepareRow: (row: Row<T>) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const rowHeight = useMemo(
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { createRef, useState } from 'react';
|
||||
import { styled, Typography, Box } from '@mui/material';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
||||
import { useExportApi } from 'hooks/api/actions/useExportApi/useExportApi';
|
||||
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';
|
||||
|
||||
interface IExportDialogProps {
|
||||
showExportDialog: boolean;
|
||||
data: FeatureSchema[];
|
||||
data: Pick<FeatureSchema, 'name'>[];
|
||||
onClose: () => void;
|
||||
environments: string[];
|
||||
}
|
||||
@ -37,13 +37,6 @@ export const ExportDialog = ({
|
||||
label: env,
|
||||
}));
|
||||
|
||||
const getPayload = () => {
|
||||
return {
|
||||
features: data.map(feature => feature.name),
|
||||
environment: selected,
|
||||
};
|
||||
};
|
||||
|
||||
const downloadFile = (json: any) => {
|
||||
const link = document.createElement('a');
|
||||
ref.current?.appendChild(link);
|
||||
@ -65,7 +58,10 @@ export const ExportDialog = ({
|
||||
|
||||
const onClick = async () => {
|
||||
try {
|
||||
const payload = getPayload();
|
||||
const payload = {
|
||||
features: data.map(feature => feature.name),
|
||||
environment: selected,
|
||||
};
|
||||
const res = await createExport(payload);
|
||||
const body = await res.json();
|
||||
downloadFile(body);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Checkbox,
|
||||
IconButton,
|
||||
styled,
|
||||
Tooltip,
|
||||
@ -8,7 +9,14 @@ import {
|
||||
} 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 {
|
||||
SortingRule,
|
||||
useFlexLayout,
|
||||
useSortBy,
|
||||
useRowSelect,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import type { FeatureSchema } from 'openapi';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
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 { 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';
|
||||
import { RowSelectCell } from './RowSelectCell/RowSelectCell';
|
||||
import { SelectionActionsBar } from './SelectionActionsBar/SelectionActionsBar';
|
||||
|
||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||
whiteSpace: 'nowrap',
|
||||
@ -96,7 +105,7 @@ type ListItemType = Pick<
|
||||
someEnabledEnvironmentHasVariants: boolean;
|
||||
};
|
||||
|
||||
const staticColumns = ['Actions', 'name', 'favorite'];
|
||||
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
||||
|
||||
const defaultSort: SortingRule<string> & {
|
||||
columns?: string[];
|
||||
@ -223,8 +232,31 @@ export const ProjectFeatureToggles = ({
|
||||
[projectId, refetch]
|
||||
);
|
||||
|
||||
const showTagsColumn = useMemo(
|
||||
() => features.some(feature => feature?.tags?.length),
|
||||
[features]
|
||||
);
|
||||
|
||||
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',
|
||||
Header: (
|
||||
@ -242,6 +274,7 @@ export const ProjectFeatureToggles = ({
|
||||
),
|
||||
maxWidth: 50,
|
||||
disableSortBy: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
{
|
||||
Header: 'Seen',
|
||||
@ -271,18 +304,21 @@ export const ProjectFeatureToggles = ({
|
||||
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,
|
||||
},
|
||||
...(showTagsColumn
|
||||
? [
|
||||
{
|
||||
id: 'tags',
|
||||
Header: 'Tags',
|
||||
accessor: (row: IFeatureToggleListItem) =>
|
||||
row.tags
|
||||
?.map(({ type, value }) => `${type}:${value}`)
|
||||
.join('\n') || '',
|
||||
Cell: FeatureTagCell,
|
||||
width: 80,
|
||||
searchable: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
@ -343,6 +379,7 @@ export const ProjectFeatureToggles = ({
|
||||
/>
|
||||
),
|
||||
disableSortBy: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
],
|
||||
[projectId, environments, loading, onToggle]
|
||||
@ -397,7 +434,7 @@ export const ProjectFeatureToggles = ({
|
||||
environments: {
|
||||
production: { name: 'production', enabled: false },
|
||||
},
|
||||
}) as object[];
|
||||
}) as FeatureSchema[];
|
||||
}
|
||||
return searchedData;
|
||||
}, [loading, searchedData]);
|
||||
@ -438,6 +475,7 @@ export const ProjectFeatureToggles = ({
|
||||
},
|
||||
],
|
||||
hiddenColumns,
|
||||
selectedRowIds: {},
|
||||
};
|
||||
},
|
||||
[environments] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
@ -449,7 +487,7 @@ export const ProjectFeatureToggles = ({
|
||||
allColumns,
|
||||
headerGroups,
|
||||
rows,
|
||||
state: { sortBy, hiddenColumns },
|
||||
state: { selectedRowIds, sortBy, hiddenColumns },
|
||||
prepareRow,
|
||||
setHiddenColumns,
|
||||
} = useTable(
|
||||
@ -464,18 +502,8 @@ export const ProjectFeatureToggles = ({
|
||||
getRowId,
|
||||
},
|
||||
useFlexLayout,
|
||||
useSortBy
|
||||
);
|
||||
|
||||
useConditionallyHiddenColumns(
|
||||
[
|
||||
{
|
||||
condition: !features.some(({ tags }) => tags?.length),
|
||||
columns: ['tags'],
|
||||
},
|
||||
],
|
||||
setHiddenColumns,
|
||||
columns
|
||||
useSortBy,
|
||||
useRowSelect
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -559,7 +587,7 @@ export const ProjectFeatureToggles = ({
|
||||
)}
|
||||
show={
|
||||
<Tooltip
|
||||
title="Export current selection"
|
||||
title="Export toggles visible in the table below"
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
@ -686,6 +714,10 @@ export const ProjectFeatureToggles = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SelectionActionsBar
|
||||
selectedIds={Object.keys(selectedRowIds)}
|
||||
data={features}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
@ -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>
|
||||
 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>
|
||||
);
|
||||
};
|
@ -28,7 +28,13 @@ import {
|
||||
} from 'interfaces/personalAPIToken';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
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 { sortTypes } from 'utils/sortTypes';
|
||||
import { CreatePersonalAPIToken } from './CreatePersonalAPIToken/CreatePersonalAPIToken';
|
||||
@ -104,65 +110,69 @@ export const PersonalAPITokensTab = () => {
|
||||
const [selectedToken, setSelectedToken] = useState<IPersonalAPIToken>();
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Description',
|
||||
accessor: 'description',
|
||||
Cell: HighlightCell,
|
||||
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} />;
|
||||
() =>
|
||||
[
|
||||
{
|
||||
Header: 'Description',
|
||||
accessor: 'description',
|
||||
Cell: HighlightCell,
|
||||
minWidth: 100,
|
||||
searchable: true,
|
||||
},
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Last seen',
|
||||
accessor: 'seenAt',
|
||||
Cell: TimeAgoCell,
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Actions',
|
||||
id: 'Actions',
|
||||
align: 'center',
|
||||
Cell: ({ row: { original: rowToken } }: any) => (
|
||||
<ActionCell>
|
||||
<Tooltip title="Delete token" arrow describeChild>
|
||||
<span>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setSelectedToken(rowToken);
|
||||
setDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ActionCell>
|
||||
),
|
||||
maxWidth: 100,
|
||||
disableSortBy: 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: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Last seen',
|
||||
accessor: 'seenAt',
|
||||
Cell: TimeAgoCell,
|
||||
sortType: 'date',
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Actions',
|
||||
id: 'Actions',
|
||||
align: 'center',
|
||||
Cell: ({ row: { original: rowToken } }: any) => (
|
||||
<ActionCell>
|
||||
<Tooltip title="Delete token" arrow describeChild>
|
||||
<span>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setSelectedToken(rowToken);
|
||||
setDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ActionCell>
|
||||
),
|
||||
maxWidth: 100,
|
||||
disableSortBy: true,
|
||||
},
|
||||
] as Column<IPersonalAPIToken>[],
|
||||
[setSelectedToken, setDeleteOpen]
|
||||
);
|
||||
|
||||
@ -186,7 +196,7 @@ export const PersonalAPITokensTab = () => {
|
||||
prepareRow,
|
||||
state: { sortBy },
|
||||
setHiddenColumns,
|
||||
} = useTable(
|
||||
} = useTable<IPersonalAPIToken>(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import useSWR from 'swr';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import { PatsSchema } from 'openapi';
|
||||
import { IPersonalAPIToken } from 'interfaces/personalAPIToken';
|
||||
|
||||
export interface IUsePersonalAPITokensOutput {
|
||||
@ -10,20 +11,15 @@ export interface IUsePersonalAPITokensOutput {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export const usePersonalAPITokens = (
|
||||
userId?: number
|
||||
): IUsePersonalAPITokensOutput => {
|
||||
const { data, error, mutate } = useSWR(
|
||||
formatApiPath(
|
||||
userId
|
||||
? `api/admin/user-admin/${userId}/pat`
|
||||
: 'api/admin/user/tokens'
|
||||
),
|
||||
export const usePersonalAPITokens = (): IUsePersonalAPITokensOutput => {
|
||||
const { data, error, mutate } = useSWR<PatsSchema>(
|
||||
formatApiPath('api/admin/user/tokens'),
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
tokens: data ? data.pats : undefined,
|
||||
// FIXME: schema issue
|
||||
tokens: data ? (data.pats as any) : undefined,
|
||||
loading: !error && !data,
|
||||
refetchTokens: () => mutate(),
|
||||
error,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { IPersonalAPIToken } from 'interfaces/personalAPIToken';
|
||||
import { PatsSchema } from 'openapi';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||
@ -11,18 +12,21 @@ export interface IUseServiceAccountTokensOutput {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export const useServiceAccountTokens = (id: number) => {
|
||||
export const useServiceAccountTokens = (
|
||||
id: number
|
||||
): IUseServiceAccountTokensOutput => {
|
||||
const { isEnterprise } = useUiConfig();
|
||||
|
||||
const { data, error, mutate } = useConditionalSWR(
|
||||
const { data, error, mutate } = useConditionalSWR<PatsSchema>(
|
||||
isEnterprise(),
|
||||
{ tokens: [] },
|
||||
{ pats: [] },
|
||||
formatApiPath(`api/admin/service-account/${id}/token`),
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
tokens: data ? data.pats : undefined,
|
||||
// FIXME: schema issue
|
||||
tokens: data ? (data.pats as any) : undefined,
|
||||
loading: !error && !data,
|
||||
refetchTokens: () => mutate(),
|
||||
error,
|
||||
|
@ -7,7 +7,7 @@ import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||
import useUiConfig from '../useUiConfig/useUiConfig';
|
||||
|
||||
export const useServiceAccounts = () => {
|
||||
const { uiConfig, isEnterprise } = useUiConfig();
|
||||
const { isEnterprise } = useUiConfig();
|
||||
|
||||
const { data, error, mutate } = useConditionalSWR(
|
||||
isEnterprise(),
|
||||
|
@ -1,29 +1,34 @@
|
||||
interface IUseSearchOutput {
|
||||
getSearchText: (input: string) => string;
|
||||
data: any[];
|
||||
getSearchContext: () => IGetSearchContextOutput;
|
||||
}
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export interface IGetSearchContextOutput {
|
||||
data: any[];
|
||||
export type IGetSearchContextOutput<T extends any = any> = {
|
||||
data: T[];
|
||||
columns: any[];
|
||||
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[],
|
||||
searchValue: string,
|
||||
data: any[]
|
||||
): IUseSearchOutput => {
|
||||
const getSearchText = getSearchTextGenerator(columns);
|
||||
data: T[]
|
||||
): IUseSearchOutput<T> => {
|
||||
const getSearchText = useCallback(
|
||||
(value: string) => getSearchTextGenerator(columns)(value),
|
||||
[columns]
|
||||
);
|
||||
|
||||
const getSearchContext = () => {
|
||||
const getSearchContext = useCallback(() => {
|
||||
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 searchedData = searchInFilteredData(
|
||||
columns,
|
||||
@ -32,9 +37,9 @@ export const useSearch = (
|
||||
);
|
||||
|
||||
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[]) => {
|
||||
@ -57,10 +62,10 @@ export const filter = (columns: any[], searchValue: string, data: any[]) => {
|
||||
return filteredDataSet;
|
||||
};
|
||||
|
||||
export const searchInFilteredData = (
|
||||
export const searchInFilteredData = <T extends any>(
|
||||
columns: any[],
|
||||
searchValue: string,
|
||||
filteredData: any[]
|
||||
filteredData: T[]
|
||||
) => {
|
||||
const searchableColumns = columns.filter(
|
||||
column => column.searchable && column.accessor
|
||||
|
@ -40,6 +40,7 @@ process.nextTick(async () => {
|
||||
responseTimeWithAppNameKillSwitch: false,
|
||||
featuresExportImport: true,
|
||||
newProjectOverview: true,
|
||||
bulkOperations: true,
|
||||
projectStatusApi: true,
|
||||
showProjectApiAccess: true,
|
||||
projectScopedSegments: true,
|
||||
|
Loading…
Reference in New Issue
Block a user