mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: display sign on log (#3193)
## About the changes https://linear.app/unleash/issue/2-719/display-basic-sign-on-log-in-frontend Adds a basic, naïve way of displaying sign-on history. We can be OK with merging this for now if we want, given that it's behind a feature flag, and then iterate over it and implement the features and improvements we need in the meantime. ![image](https://user-images.githubusercontent.com/14320932/221217564-90868ce8-6608-4f30-b2e4-88f72f1e4ac6.png) <!-- (For internal contributors): Does it relate to an issue on public roadmap? --> Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: #2951
This commit is contained in:
parent
e7ef06ff9d
commit
a43542b0d1
@ -1,7 +1,7 @@
|
|||||||
import { Tooltip, Typography } from '@mui/material';
|
import { Tooltip, Typography } from '@mui/material';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { formatDateYMD } from 'utils/formatDate';
|
import { formatDateYMD, formatDateYMDHMS } from 'utils/formatDate';
|
||||||
import { TextCell } from '../TextCell/TextCell';
|
import { TextCell } from '../TextCell/TextCell';
|
||||||
import TimeAgo from 'react-timeago';
|
import TimeAgo from 'react-timeago';
|
||||||
|
|
||||||
@ -10,6 +10,7 @@ interface ITimeAgoCellProps {
|
|||||||
live?: boolean;
|
live?: boolean;
|
||||||
emptyText?: string;
|
emptyText?: string;
|
||||||
title?: (date: string) => string;
|
title?: (date: string) => string;
|
||||||
|
timestamp?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimeAgoCell: VFC<ITimeAgoCellProps> = ({
|
export const TimeAgoCell: VFC<ITimeAgoCellProps> = ({
|
||||||
@ -17,12 +18,15 @@ export const TimeAgoCell: VFC<ITimeAgoCellProps> = ({
|
|||||||
live = false,
|
live = false,
|
||||||
emptyText,
|
emptyText,
|
||||||
title,
|
title,
|
||||||
|
timestamp,
|
||||||
}) => {
|
}) => {
|
||||||
const { locationSettings } = useLocationSettings();
|
const { locationSettings } = useLocationSettings();
|
||||||
|
|
||||||
if (!value) return <TextCell>{emptyText}</TextCell>;
|
if (!value) return <TextCell>{emptyText}</TextCell>;
|
||||||
|
|
||||||
const date = formatDateYMD(value, locationSettings.locale);
|
const date = timestamp
|
||||||
|
? formatDateYMDHMS(value, locationSettings.locale)
|
||||||
|
: formatDateYMD(value, locationSettings.locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextCell>
|
<TextCell>
|
||||||
|
@ -347,6 +347,15 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Event log",
|
"title": "Event log",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"component": [Function],
|
||||||
|
"menu": {
|
||||||
|
"adminSettings": true,
|
||||||
|
},
|
||||||
|
"path": "/admin/sign-on-log",
|
||||||
|
"title": "Sign on log",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"menu": {},
|
"menu": {},
|
||||||
|
@ -43,6 +43,7 @@ import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
|
|||||||
import { LazyAdmin } from 'component/admin/LazyAdmin';
|
import { LazyAdmin } from 'component/admin/LazyAdmin';
|
||||||
import { LazyProject } from 'component/project/Project/LazyProject';
|
import { LazyProject } from 'component/project/Project/LazyProject';
|
||||||
import { AdminRedirect } from 'component/admin/AdminRedirect';
|
import { AdminRedirect } from 'component/admin/AdminRedirect';
|
||||||
|
import { SignOnLog } from 'component/signOnLog/SignOnLog';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -355,6 +356,14 @@ export const routes: IRoute[] = [
|
|||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/admin/sign-on-log',
|
||||||
|
title: 'Sign on log',
|
||||||
|
component: SignOnLog,
|
||||||
|
type: 'protected',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
},
|
||||||
|
|
||||||
// Archive
|
// Archive
|
||||||
{
|
{
|
||||||
path: '/archive',
|
path: '/archive',
|
||||||
@ -438,6 +447,12 @@ export const adminMenuRoutes: INavigationMenuItem[] = [
|
|||||||
title: 'Event log',
|
title: 'Event log',
|
||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/sign-on-log',
|
||||||
|
title: 'Sign-on log',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
flag: 'loginEventLog',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/users',
|
path: '/admin/users',
|
||||||
title: 'Users',
|
title: 'Users',
|
||||||
|
20
frontend/src/component/signOnLog/SignOnLog.tsx
Normal file
20
frontend/src/component/signOnLog/SignOnLog.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import AccessContext from 'contexts/AccessContext';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
|
||||||
|
import { SignOnLogTable } from './SignOnLogTable/SignOnLogTable';
|
||||||
|
|
||||||
|
export const SignOnLog = () => {
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(ADMIN)}
|
||||||
|
show={<SignOnLogTable />}
|
||||||
|
elseShow={<AdminAlert />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Delete } from '@mui/icons-material';
|
||||||
|
import { Box, styled } from '@mui/material';
|
||||||
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface ISignOnLogActionsCellProps {
|
||||||
|
onDelete: (event: React.SyntheticEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SignOnLogActionsCell = ({
|
||||||
|
onDelete,
|
||||||
|
}: ISignOnLogActionsCellProps) => {
|
||||||
|
return (
|
||||||
|
<StyledBox>
|
||||||
|
<PermissionIconButton
|
||||||
|
data-loading
|
||||||
|
onClick={onDelete}
|
||||||
|
permission={ADMIN}
|
||||||
|
tooltipProps={{
|
||||||
|
title: 'Remove event',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</PermissionIconButton>
|
||||||
|
</StyledBox>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import { ISignOnEvent } from 'interfaces/signOnEvent';
|
||||||
|
|
||||||
|
interface IServiceAccountDeleteDialogProps {
|
||||||
|
event?: ISignOnEvent;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
onConfirm: (event: ISignOnEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SignOnLogDeleteDialog = ({
|
||||||
|
event,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
onConfirm,
|
||||||
|
}: IServiceAccountDeleteDialogProps) => (
|
||||||
|
<Dialogue
|
||||||
|
title="Delete event?"
|
||||||
|
open={open}
|
||||||
|
primaryButtonText="Delete event"
|
||||||
|
secondaryButtonText="Cancel"
|
||||||
|
onClick={() => onConfirm(event!)}
|
||||||
|
onClose={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You are about to delete event: <strong>#{event?.id}</strong>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
@ -0,0 +1,48 @@
|
|||||||
|
import { VFC } from 'react';
|
||||||
|
import { Box, styled } from '@mui/material';
|
||||||
|
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||||
|
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { ISignOnEvent } from 'interfaces/signOnEvent';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface ISignOnLogSuccessfulCellProps {
|
||||||
|
row: {
|
||||||
|
original: ISignOnEvent;
|
||||||
|
};
|
||||||
|
value: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SignOnLogSuccessfulCell: VFC<ISignOnLogSuccessfulCellProps> = ({
|
||||||
|
row,
|
||||||
|
value,
|
||||||
|
}) => {
|
||||||
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
return (
|
||||||
|
<StyledBox>
|
||||||
|
<Badge color="success">True</Badge>
|
||||||
|
</StyledBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledBox>
|
||||||
|
<HtmlTooltip
|
||||||
|
arrow
|
||||||
|
title={
|
||||||
|
<Highlighter search={searchQuery}>
|
||||||
|
{row.original.failure_reason}
|
||||||
|
</Highlighter>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Badge color="error">False</Badge>
|
||||||
|
</HtmlTooltip>
|
||||||
|
</StyledBox>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,234 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { useMediaQuery } from '@mui/material';
|
||||||
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { useFlexLayout, useSortBy, useTable } from 'react-table';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
|
import { useSearch } from 'hooks/useSearch';
|
||||||
|
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
||||||
|
import { useSignOnLog } from 'hooks/api/getters/useSignOnLog/useSignOnLog';
|
||||||
|
import { SignOnLogSuccessfulCell } from './SignOnLogSuccessfulCell/SignOnLogSuccessfulCell';
|
||||||
|
import { ISignOnEvent } from 'interfaces/signOnEvent';
|
||||||
|
import { SignOnLogActionsCell } from './SignOnLogActionsCell/SignOnLogActionsCell';
|
||||||
|
import { SignOnLogDeleteDialog } from './SignOnLogDeleteDialog/SignOnLogDeleteDialog';
|
||||||
|
import { useSignOnLogApi } from 'hooks/api/actions/useSignOnLogApi/useSignOnLogApi';
|
||||||
|
|
||||||
|
export const SignOnLogTable = () => {
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
|
||||||
|
const { events, loading, refetch } = useSignOnLog();
|
||||||
|
const { removeEvent } = useSignOnLogApi();
|
||||||
|
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState<ISignOnEvent>();
|
||||||
|
|
||||||
|
const onDeleteConfirm = async (event: ISignOnEvent) => {
|
||||||
|
try {
|
||||||
|
await removeEvent(event.id);
|
||||||
|
setToastData({
|
||||||
|
title: `Event has been deleted`,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
|
setDeleteOpen(false);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: 'Created',
|
||||||
|
accessor: 'created_at',
|
||||||
|
Cell: ({ value }: { value: Date }) => (
|
||||||
|
<TimeAgoCell value={value} timestamp />
|
||||||
|
),
|
||||||
|
sortType: 'date',
|
||||||
|
maxWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Username',
|
||||||
|
accessor: 'username',
|
||||||
|
minWidth: 100,
|
||||||
|
Cell: HighlightCell,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Authentication',
|
||||||
|
accessor: (event: ISignOnEvent) =>
|
||||||
|
event.auth_type === 'simple'
|
||||||
|
? 'Password'
|
||||||
|
: event.auth_type.toUpperCase(),
|
||||||
|
width: 150,
|
||||||
|
maxWidth: 150,
|
||||||
|
Cell: HighlightCell,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'IP address',
|
||||||
|
accessor: 'ip',
|
||||||
|
Cell: HighlightCell,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Successful',
|
||||||
|
accessor: 'successful',
|
||||||
|
align: 'center',
|
||||||
|
Cell: SignOnLogSuccessfulCell,
|
||||||
|
filterName: 'successful',
|
||||||
|
filterParsing: (value: boolean) => value.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Actions',
|
||||||
|
id: 'Actions',
|
||||||
|
align: 'center',
|
||||||
|
Cell: ({ row: { original: event } }: any) => (
|
||||||
|
<SignOnLogActionsCell
|
||||||
|
onDelete={() => {
|
||||||
|
setSelectedEvent(event);
|
||||||
|
setDeleteOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
width: 150,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
// Always hidden -- for search
|
||||||
|
{
|
||||||
|
accessor: 'failure_reason',
|
||||||
|
Header: 'Failure Reason',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [initialState] = useState({
|
||||||
|
sortBy: [{ id: 'created_at' }],
|
||||||
|
hiddenColumns: ['failure_reason'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, getSearchText, getSearchContext } = useSearch(
|
||||||
|
columns,
|
||||||
|
searchValue,
|
||||||
|
events
|
||||||
|
);
|
||||||
|
|
||||||
|
const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable(
|
||||||
|
{
|
||||||
|
columns: columns as any,
|
||||||
|
data,
|
||||||
|
initialState,
|
||||||
|
sortTypes,
|
||||||
|
autoResetHiddenColumns: false,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
disableMultiSort: true,
|
||||||
|
defaultColumn: {
|
||||||
|
Cell: TextCell,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
useSortBy,
|
||||||
|
useFlexLayout
|
||||||
|
);
|
||||||
|
|
||||||
|
useConditionallyHiddenColumns(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: isExtraSmallScreen,
|
||||||
|
columns: ['role', 'seenAt'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: isSmallScreen,
|
||||||
|
columns: ['imageUrl', 'tokens', 'createdAt'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
setHiddenColumns,
|
||||||
|
columns
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent
|
||||||
|
isLoading={loading}
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
title={`Sign-on log (${rows.length})`}
|
||||||
|
actions={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
hasFilters
|
||||||
|
getSearchContext={getSearchContext}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
hasFilters
|
||||||
|
getSearchContext={getSearchContext}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageHeader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||||
|
<VirtualizedTable
|
||||||
|
rows={rows}
|
||||||
|
headerGroups={headerGroups}
|
||||||
|
prepareRow={prepareRow}
|
||||||
|
/>
|
||||||
|
</SearchHighlightProvider>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={rows.length === 0}
|
||||||
|
show={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={searchValue?.length > 0}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No sign-on events found matching “
|
||||||
|
{searchValue}
|
||||||
|
”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No sign-on events available.
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SignOnLogDeleteDialog
|
||||||
|
event={selectedEvent}
|
||||||
|
open={deleteOpen}
|
||||||
|
setOpen={setDeleteOpen}
|
||||||
|
onConfirm={onDeleteConfirm}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
|
export const useSignOnLogApi = () => {
|
||||||
|
const { loading, makeRequest, createRequest, errors } = useAPI({
|
||||||
|
propagateErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeEvent = async (eventId: number) => {
|
||||||
|
const requestId = 'removeEvent';
|
||||||
|
const req = createRequest(
|
||||||
|
`api/admin/login-event/${eventId}`,
|
||||||
|
{ method: 'DELETE' },
|
||||||
|
requestId
|
||||||
|
);
|
||||||
|
|
||||||
|
await makeRequest(req.caller, req.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
removeEvent,
|
||||||
|
errors,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
35
frontend/src/hooks/api/getters/useSignOnLog/useSignOnLog.ts
Normal file
35
frontend/src/hooks/api/getters/useSignOnLog/useSignOnLog.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { ISignOnEvent } from 'interfaces/signOnEvent';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||||
|
import useUiConfig from '../useUiConfig/useUiConfig';
|
||||||
|
|
||||||
|
export const useSignOnLog = () => {
|
||||||
|
const { uiConfig, isEnterprise } = useUiConfig();
|
||||||
|
|
||||||
|
const { loginEventLog } = uiConfig.flags;
|
||||||
|
|
||||||
|
const { data, error, mutate } = useConditionalSWR(
|
||||||
|
loginEventLog && isEnterprise(),
|
||||||
|
[],
|
||||||
|
formatApiPath(`api/admin/login-event`),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
events: (data ?? []) as ISignOnEvent[],
|
||||||
|
loading: !error && !data,
|
||||||
|
refetch: () => mutate(),
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
[data, error, mutate]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetcher = (path: string) => {
|
||||||
|
return fetch(path)
|
||||||
|
.then(handleErrorResponses('Sign-On Log'))
|
||||||
|
.then(res => res.json());
|
||||||
|
};
|
9
frontend/src/interfaces/signOnEvent.ts
Normal file
9
frontend/src/interfaces/signOnEvent.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface ISignOnEvent {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
auth_type: string;
|
||||||
|
created_at: Date;
|
||||||
|
successful: boolean;
|
||||||
|
ip?: string;
|
||||||
|
failure_reason?: string;
|
||||||
|
}
|
@ -48,6 +48,7 @@ export interface IFlags {
|
|||||||
showProjectApiAccess?: boolean;
|
showProjectApiAccess?: boolean;
|
||||||
proPlanAutoCharge?: boolean;
|
proPlanAutoCharge?: boolean;
|
||||||
notifications?: boolean;
|
notifications?: boolean;
|
||||||
|
loginEventLog?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
Loading…
Reference in New Issue
Block a user