From 30a753b93f0353228753dab4cad899e44a5c987f Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Tue, 14 Mar 2023 09:56:03 +0100 Subject: [PATCH] UI/bulk select (#3267) Select multiple toggles on project overview. --- .../ServiceAccountTokens.tsx | 128 +++++++++-------- .../SortableTableHeader.tsx | 15 +- .../VirtualizedTable/VirtualizedTable.tsx | 16 +-- .../FeatureToggleList/ExportDialog.tsx | 18 +-- .../ProjectFeatureToggles.tsx | 92 +++++++++---- .../RowSelectCell/RowSelectCell.tsx | 24 ++++ .../SelectionActionsBar.tsx | 120 ++++++++++++++++ .../PersonalAPITokensTab.tsx | 130 ++++++++++-------- .../usePersonalAPITokens.ts | 16 +-- .../useServiceAccountTokens.ts | 12 +- .../useServiceAccounts/useServiceAccounts.ts | 2 +- frontend/src/hooks/useSearch.ts | 45 +++--- src/server-dev.ts | 1 + 13 files changed, 406 insertions(+), 213 deletions(-) create mode 100644 frontend/src/component/project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell.tsx create mode 100644 frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/SelectionActionsBar.tsx diff --git a/frontend/src/component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountModal/ServiceAccountTokens/ServiceAccountTokens.tsx b/frontend/src/component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountModal/ServiceAccountTokens/ServiceAccountTokens.tsx index 3468ef469f..5ecdcce773 100644 --- a/frontend/src/component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountModal/ServiceAccountTokens/ServiceAccountTokens.tsx +++ b/frontend/src/component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountModal/ServiceAccountTokens/ServiceAccountTokens.tsx @@ -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 Never; - } - return ; + () => + [ + { + 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) => ( - - - - { - setSelectedToken(rowToken); - setDeleteOpen(true); - }} - > - - - - - - ), - 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 Never; + } + return ; + }, + 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) => ( + + + + { + setSelectedToken(rowToken); + setDeleteOpen(true); + }} + > + + + + + + ), + maxWidth: 100, + disableSortBy: true, + }, + ] as Column[], [setSelectedToken, setDeleteOpen] ); diff --git a/frontend/src/component/common/Table/SortableTableHeader/SortableTableHeader.tsx b/frontend/src/component/common/Table/SortableTableHeader/SortableTableHeader.tsx index c120624e15..499f708d98 100644 --- a/frontend/src/component/common/Table/SortableTableHeader/SortableTableHeader.tsx +++ b/frontend/src/component/common/Table/SortableTableHeader/SortableTableHeader.tsx @@ -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[]; - className?: string; - flex?: boolean; -} - -export const SortableTableHeader: VFC = ({ +export const SortableTableHeader = ({ headerGroups, className, flex, +}: { + headerGroups: HeaderGroup[]; + className?: string; + flex?: boolean; }) => ( {headerGroups.map(headerGroup => ( - {headerGroup.headers.map((column: HeaderGroup) => { + {headerGroup.headers.map((column: HeaderGroup) => { const content = column.render('Header'); return ( diff --git a/frontend/src/component/common/Table/VirtualizedTable/VirtualizedTable.tsx b/frontend/src/component/common/Table/VirtualizedTable/VirtualizedTable.tsx index 13b992dde3..69ebaae634 100644 --- a/frontend/src/component/common/Table/VirtualizedTable/VirtualizedTable.tsx +++ b/frontend/src/component/common/Table/VirtualizedTable/VirtualizedTable.tsx @@ -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[]; - rows: Row[]; - 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 = ({ +export const VirtualizedTable = ({ rowHeight: rowHeightOverride, headerGroups, rows, prepareRow, +}: { + rowHeight?: number; + headerGroups: HeaderGroup[]; + rows: Row[]; + prepareRow: (row: Row) => void; }) => { const theme = useTheme(); const rowHeight = useMemo( diff --git a/frontend/src/component/feature/FeatureToggleList/ExportDialog.tsx b/frontend/src/component/feature/FeatureToggleList/ExportDialog.tsx index fd001ee445..2d8a2a5902 100644 --- a/frontend/src/component/feature/FeatureToggleList/ExportDialog.tsx +++ b/frontend/src/component/feature/FeatureToggleList/ExportDialog.tsx @@ -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[]; 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); diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index 89e1c99df1..7e9d18ff19 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -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 & { 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) => ( + + ), + Cell: ({ row }: any) => ( + + ), + 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={ } /> + ); }; diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell.tsx new file mode 100644 index 0000000000..eaac3ba5ff --- /dev/null +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell.tsx @@ -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 = ({ + onChange, + checked, + title, +}) => ( + + + +); diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/SelectionActionsBar.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/SelectionActionsBar.tsx new file mode 100644 index 0000000000..27525e5868 --- /dev/null +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/SelectionActionsBar.tsx @@ -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 = ({ + 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 ( + + + + {selectedIds.length} +  selected + + + + + + + setShowExportDialog(false)} + environments={environments} + /> + } + /> + + ); +}; diff --git a/frontend/src/component/user/Profile/PersonalAPITokensTab/PersonalAPITokensTab.tsx b/frontend/src/component/user/Profile/PersonalAPITokensTab/PersonalAPITokensTab.tsx index 1b2ec5b89c..747b8d1e20 100644 --- a/frontend/src/component/user/Profile/PersonalAPITokensTab/PersonalAPITokensTab.tsx +++ b/frontend/src/component/user/Profile/PersonalAPITokensTab/PersonalAPITokensTab.tsx @@ -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(); 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 Never; - } - return ; + () => + [ + { + 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) => ( - - - - { - setSelectedToken(rowToken); - setDeleteOpen(true); - }} - > - - - - - - ), - 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 Never; + } + return ; + }, + 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) => ( + + + + { + setSelectedToken(rowToken); + setDeleteOpen(true); + }} + > + + + + + + ), + maxWidth: 100, + disableSortBy: true, + }, + ] as Column[], [setSelectedToken, setDeleteOpen] ); @@ -186,7 +196,7 @@ export const PersonalAPITokensTab = () => { prepareRow, state: { sortBy }, setHiddenColumns, - } = useTable( + } = useTable( { columns, data, diff --git a/frontend/src/hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens.ts b/frontend/src/hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens.ts index 6f6fc38876..52e1dd8b4e 100644 --- a/frontend/src/hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens.ts +++ b/frontend/src/hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens.ts @@ -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( + 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, diff --git a/frontend/src/hooks/api/getters/useServiceAccountTokens/useServiceAccountTokens.ts b/frontend/src/hooks/api/getters/useServiceAccountTokens/useServiceAccountTokens.ts index b3e4e4b3cf..6287fbf299 100644 --- a/frontend/src/hooks/api/getters/useServiceAccountTokens/useServiceAccountTokens.ts +++ b/frontend/src/hooks/api/getters/useServiceAccountTokens/useServiceAccountTokens.ts @@ -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( 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, diff --git a/frontend/src/hooks/api/getters/useServiceAccounts/useServiceAccounts.ts b/frontend/src/hooks/api/getters/useServiceAccounts/useServiceAccounts.ts index 9e709b51ef..54de390260 100644 --- a/frontend/src/hooks/api/getters/useServiceAccounts/useServiceAccounts.ts +++ b/frontend/src/hooks/api/getters/useServiceAccounts/useServiceAccounts.ts @@ -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(), diff --git a/frontend/src/hooks/useSearch.ts b/frontend/src/hooks/useSearch.ts index c8bce302c9..f15bf2fb64 100644 --- a/frontend/src/hooks/useSearch.ts +++ b/frontend/src/hooks/useSearch.ts @@ -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 = { + data: T[]; columns: any[]; searchValue: string; -} +}; -export const useSearch = ( +type IUseSearchOutput = { + getSearchText: (input: string) => string; + data: T[]; + getSearchContext: () => IGetSearchContextOutput; +}; + +export const useSearch = ( columns: any[], searchValue: string, - data: any[] -): IUseSearchOutput => { - const getSearchText = getSearchTextGenerator(columns); + data: T[] +): IUseSearchOutput => { + 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 = ( columns: any[], searchValue: string, - filteredData: any[] + filteredData: T[] ) => { const searchableColumns = columns.filter( column => column.searchable && column.accessor diff --git a/src/server-dev.ts b/src/server-dev.ts index 065ead6646..6bd1433b4f 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -40,6 +40,7 @@ process.nextTick(async () => { responseTimeWithAppNameKillSwitch: false, featuresExportImport: true, newProjectOverview: true, + bulkOperations: true, projectStatusApi: true, showProjectApiAccess: true, projectScopedSegments: true,