mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
Feat/instance health (#4586)
Hackathon --------- Co-authored-by: chriswk <chriswk@getunleash.io>
This commit is contained in:
parent
3b2d6a4cbf
commit
0d5f33f8ba
@ -18,6 +18,7 @@ import UsersAdmin from './users/UsersAdmin';
|
||||
import NotFound from 'component/common/NotFound/NotFound';
|
||||
import { AdminIndex } from './AdminIndex';
|
||||
import { AdminTabsMenu } from './menu/AdminTabsMenu';
|
||||
import { InstanceHealth } from './instance-health/InstanceHealth';
|
||||
|
||||
export const Admin = () => {
|
||||
return (
|
||||
@ -34,6 +35,7 @@ export const Admin = () => {
|
||||
<Route path="groups/*" element={<GroupsAdmin />} />
|
||||
<Route path="roles/*" element={<Roles />} />
|
||||
<Route path="instance" element={<InstanceAdmin />} />
|
||||
<Route path="instance-health" element={<InstanceHealth />} />
|
||||
<Route path="network/*" element={<Network />} />
|
||||
<Route path="maintenance" element={<MaintenanceAdmin />} />
|
||||
<Route path="cors" element={<CorsAdmin />} />
|
||||
|
@ -80,6 +80,13 @@ export const adminRoutes: INavigationMenuItem[] = [
|
||||
menu: { adminSettings: true },
|
||||
group: 'instance',
|
||||
},
|
||||
{
|
||||
path: '/admin/instance-health',
|
||||
title: 'Instance health',
|
||||
menu: { adminSettings: true },
|
||||
group: 'instance',
|
||||
flag: 'instanceHealthDashboard',
|
||||
},
|
||||
{
|
||||
path: '/admin/instance-privacy',
|
||||
title: 'Instance privacy',
|
||||
|
@ -13,6 +13,7 @@ import { useInstanceStats } from '../../../../hooks/api/getters/useInstanceStats
|
||||
import { formatApiPath } from '../../../../utils/formatPath';
|
||||
import { PageContent } from '../../../common/PageContent/PageContent';
|
||||
import { PageHeader } from '../../../common/PageHeader/PageHeader';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
export const InstanceStats: VFC = () => {
|
||||
const { stats } = useInstanceStats();
|
||||
@ -32,6 +33,21 @@ export const InstanceStats: VFC = () => {
|
||||
{ title: 'Instance Id', value: stats?.instanceId },
|
||||
{ title: versionTitle, value: version },
|
||||
{ title: 'Users', value: stats?.users },
|
||||
{
|
||||
title: 'Active past 7 days',
|
||||
value: stats?.activeUsers?.last7,
|
||||
offset: true,
|
||||
},
|
||||
{
|
||||
title: 'Active past 30 days',
|
||||
value: stats?.activeUsers?.last30,
|
||||
offset: true,
|
||||
},
|
||||
{
|
||||
title: 'Active past 90 days',
|
||||
value: stats?.activeUsers?.last90,
|
||||
offset: true,
|
||||
},
|
||||
{ title: 'Feature toggles', value: stats?.featureToggles },
|
||||
{ title: 'Projects', value: stats?.projects },
|
||||
{ title: 'Environments', value: stats?.environments },
|
||||
@ -64,7 +80,22 @@ export const InstanceStats: VFC = () => {
|
||||
{rows.map(row => (
|
||||
<TableRow key={row.title}>
|
||||
<TableCell component="th" scope="row">
|
||||
{row.title}
|
||||
<ConditionallyRender
|
||||
condition={Boolean(row.offset)}
|
||||
show={
|
||||
<Box
|
||||
component="span"
|
||||
sx={theme => ({
|
||||
marginLeft: row.offset
|
||||
? theme.spacing(2)
|
||||
: 0,
|
||||
})}
|
||||
>
|
||||
{row.title}
|
||||
</Box>
|
||||
}
|
||||
elseShow={row.title}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">{row.value}</TableCell>
|
||||
</TableRow>
|
||||
|
220
frontend/src/component/admin/instance-health/InstanceHealth.tsx
Normal file
220
frontend/src/component/admin/instance-health/InstanceHealth.tsx
Normal file
@ -0,0 +1,220 @@
|
||||
import { VFC, useMemo } from 'react';
|
||||
import { useSortBy, useTable } from 'react-table';
|
||||
import { styled, Typography, Box } from '@mui/material';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { useInstanceStats } from 'hooks/api/getters/useInstanceStats/useInstanceStats';
|
||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import {
|
||||
SortableTableHeader,
|
||||
Table,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from 'component/common/Table';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { HelpPopper } from 'component/project/Project/ProjectStats/HelpPopper';
|
||||
import { StatusBox } from 'component/project/Project/ProjectStats/StatusBox';
|
||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||
|
||||
interface IInstanceHealthProps {}
|
||||
|
||||
const CardsGrid = styled('div')(({ theme }) => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||
gap: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const Card = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(2.5),
|
||||
borderRadius: `${theme.shape.borderRadiusLarge}px`,
|
||||
backgroundColor: `${theme.palette.background.paper}`,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
// boxShadow: theme.boxShadows.card,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated unify with project widget
|
||||
*/
|
||||
const StyledWidget = styled(Box)(({ theme }) => ({
|
||||
position: 'relative',
|
||||
padding: theme.spacing(3),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: `${theme.shape.borderRadiusLarge}px`,
|
||||
[theme.breakpoints.down('lg')]: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
export const InstanceHealth: VFC<IInstanceHealthProps> = () => {
|
||||
const { stats } = useInstanceStats();
|
||||
const { projects } = useProjects();
|
||||
// FIXME: loading state
|
||||
|
||||
const initialState = useMemo(
|
||||
() => ({
|
||||
hiddenColumns: [],
|
||||
sortBy: [{ id: 'createdAt' }],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const data = useMemo(() => projects, [projects]);
|
||||
|
||||
const dormantUsersPercentage =
|
||||
(1 - stats?.activeUsers?.last90! / stats?.users!) * 100;
|
||||
|
||||
const dormantUsersColor =
|
||||
dormantUsersPercentage < 30
|
||||
? 'success.main'
|
||||
: dormantUsersPercentage < 50
|
||||
? 'warning.main'
|
||||
: 'error.main';
|
||||
|
||||
const COLUMNS = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessor: 'name',
|
||||
Header: 'Project name',
|
||||
Cell: TextCell,
|
||||
width: '80%',
|
||||
},
|
||||
{
|
||||
Header: 'Feature toggles',
|
||||
accessor: 'featureCount',
|
||||
Cell: TextCell,
|
||||
},
|
||||
{
|
||||
Header: 'Created at',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
},
|
||||
{
|
||||
Header: 'Members',
|
||||
accessor: 'memberCount',
|
||||
Cell: TextCell,
|
||||
},
|
||||
{
|
||||
Header: 'Health',
|
||||
accessor: 'health',
|
||||
Cell: ({ value }: { value: number }) => {
|
||||
const healthRatingColor =
|
||||
value < 50
|
||||
? 'error.main'
|
||||
: value < 75
|
||||
? 'warning.main'
|
||||
: 'success.main';
|
||||
return (
|
||||
<TextCell>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ color: healthRatingColor }}
|
||||
>
|
||||
{value}%
|
||||
</Typography>
|
||||
</TextCell>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const { headerGroups, rows, prepareRow, getTableProps, getTableBodyProps } =
|
||||
useTable(
|
||||
{
|
||||
columns: COLUMNS as any,
|
||||
data: data as any,
|
||||
initialState,
|
||||
sortTypes,
|
||||
autoResetGlobalFilter: false,
|
||||
autoResetHiddenColumns: false,
|
||||
autoResetSortBy: false,
|
||||
disableSortRemove: true,
|
||||
},
|
||||
useSortBy
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardsGrid>
|
||||
<Card>
|
||||
<StatusBox
|
||||
title="User accounts"
|
||||
boxText={String(stats?.users)}
|
||||
customChangeElement={<></>}
|
||||
>
|
||||
{/* <HelpPopper id="user-accounts">
|
||||
Sum of all configuration and state modifications in
|
||||
the project.
|
||||
</HelpPopper> */}
|
||||
{/* FIXME: tooltip */}
|
||||
</StatusBox>
|
||||
</Card>
|
||||
<Card>
|
||||
<StatusBox
|
||||
title="Dormant users"
|
||||
boxText={String(
|
||||
stats?.users! - stats?.activeUsers?.last90!
|
||||
)}
|
||||
customChangeElement={
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ color: dormantUsersColor }}
|
||||
>
|
||||
({dormantUsersPercentage.toFixed(1)}%)
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
{/* <HelpPopper id="dormant-users">
|
||||
Sum of all configuration and state modifications in
|
||||
the project.
|
||||
</HelpPopper> */}
|
||||
</StatusBox>
|
||||
</Card>
|
||||
<Card>
|
||||
<StatusBox
|
||||
title="Number of projects"
|
||||
boxText={String(projects?.length)}
|
||||
customChangeElement={<></>}
|
||||
></StatusBox>
|
||||
</Card>
|
||||
<Card>
|
||||
<StatusBox
|
||||
title="Number of feature toggles"
|
||||
boxText={String(stats?.featureToggles)}
|
||||
customChangeElement={<></>}
|
||||
></StatusBox>
|
||||
</Card>
|
||||
</CardsGrid>
|
||||
<PageContent header={<PageHeader title="Health per project" />}>
|
||||
<Table {...getTableProps()} rowHeight="standard">
|
||||
<SortableTableHeader headerGroups={headerGroups} />
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{rows.map(row => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<TableRow hover {...row.getRowProps()}>
|
||||
{row.cells.map(cell => (
|
||||
<TableCell {...cell.getCellProps()}>
|
||||
{cell.render('Cell')}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
};
|
@ -2,29 +2,10 @@ import useSWR from 'swr';
|
||||
import { useMemo } from 'react';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
interface InstanceStats {
|
||||
instanceId: string;
|
||||
timestamp: Date;
|
||||
versionOSS: string;
|
||||
versionEnterprise?: string;
|
||||
users: number;
|
||||
featureToggles: number;
|
||||
projects: number;
|
||||
contextFields: number;
|
||||
roles: number;
|
||||
groups: number;
|
||||
environments: number;
|
||||
segments: number;
|
||||
strategies: number;
|
||||
featureExports: number;
|
||||
featureImports: number;
|
||||
SAMLenabled: boolean;
|
||||
OIDCenabled: boolean;
|
||||
}
|
||||
import { InstanceAdminStatsSchema } from 'openapi';
|
||||
|
||||
export interface IInstanceStatsResponse {
|
||||
stats?: InstanceStats;
|
||||
stats?: InstanceAdminStatsSchema;
|
||||
refetchGroup: () => void;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
|
@ -59,6 +59,7 @@ export interface IFlags {
|
||||
integrationsRework?: boolean;
|
||||
multipleRoles?: boolean;
|
||||
doraMetrics?: boolean;
|
||||
instanceHealthDashboard?: boolean;
|
||||
[key: string]: boolean | Variant | undefined;
|
||||
}
|
||||
|
||||
|
@ -548,6 +548,7 @@ export * from './importTogglesSchema';
|
||||
export * from './importTogglesValidateItemSchema';
|
||||
export * from './importTogglesValidateSchema';
|
||||
export * from './instanceAdminStatsSchema';
|
||||
export * from './instanceAdminStatsSchemaActiveUsers';
|
||||
export * from './instanceAdminStatsSchemaClientAppsItem';
|
||||
export * from './instanceAdminStatsSchemaClientAppsItemRange';
|
||||
export * from './invoicesSchema';
|
||||
|
@ -3,6 +3,7 @@
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
import type { InstanceAdminStatsSchemaActiveUsers } from './instanceAdminStatsSchemaActiveUsers';
|
||||
import type { InstanceAdminStatsSchemaClientAppsItem } from './instanceAdminStatsSchemaClientAppsItem';
|
||||
|
||||
/**
|
||||
@ -19,6 +20,8 @@ export interface InstanceAdminStatsSchema {
|
||||
versionEnterprise?: string;
|
||||
/** The number of users this instance has */
|
||||
users?: number;
|
||||
/** The number of active users in the last 7, 30 and 90 days */
|
||||
activeUsers?: InstanceAdminStatsSchemaActiveUsers;
|
||||
/** The number of feature-toggles this instance has */
|
||||
featureToggles?: number;
|
||||
/** The number of projects defined in this instance. */
|
||||
@ -41,6 +44,10 @@ export interface InstanceAdminStatsSchema {
|
||||
OIDCenabled?: boolean;
|
||||
/** A count of connected applications in the last week, last month and all time since last restart */
|
||||
clientApps?: InstanceAdminStatsSchemaClientAppsItem[];
|
||||
/** The number of export operations on this instance */
|
||||
featureExports?: number;
|
||||
/** The number of import operations on this instance */
|
||||
featureImports?: number;
|
||||
/** A SHA-256 checksum of the instance statistics to be used to verify that the data in this object has not been tampered with */
|
||||
sum?: string;
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
/**
|
||||
* The number of active users in the last 7, 30 and 90 days
|
||||
*/
|
||||
export type InstanceAdminStatsSchemaActiveUsers = {
|
||||
/** The number of active users in the last 7 days */
|
||||
last7?: number;
|
||||
/** The number of active users in the last 30 days */
|
||||
last30?: number;
|
||||
/** The number of active users in the last 90 days */
|
||||
last90?: number;
|
||||
};
|
@ -201,6 +201,31 @@ class UserStore implements IUserStore {
|
||||
.then((res) => Number(res[0].count));
|
||||
}
|
||||
|
||||
async getActiveUsersCount(): Promise<{
|
||||
last7: number;
|
||||
last30: number;
|
||||
last90: number;
|
||||
}> {
|
||||
const result = await this.db.raw(
|
||||
`SELECT
|
||||
(SELECT COUNT(*) FROM ${TABLE} WHERE seen_at > NOW() - INTERVAL '1 week') AS last_week,
|
||||
(SELECT COUNT(*) FROM ${TABLE} WHERE seen_at > NOW() - INTERVAL '1 month') AS last_month,
|
||||
(SELECT COUNT(*) FROM ${TABLE} WHERE seen_at > NOW() - INTERVAL '3 months') AS last_quarter`,
|
||||
);
|
||||
|
||||
const {
|
||||
last_week: last7,
|
||||
last_month: last30,
|
||||
last_quarter: last90,
|
||||
} = result.rows[0];
|
||||
|
||||
return {
|
||||
last7,
|
||||
last30,
|
||||
last90,
|
||||
};
|
||||
}
|
||||
|
||||
destroy(): void {}
|
||||
|
||||
async exists(id: number): Promise<boolean> {
|
||||
|
@ -39,6 +39,34 @@ export const instanceAdminStatsSchema = {
|
||||
example: 8,
|
||||
minimum: 0,
|
||||
},
|
||||
activeUsers: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The number of active users in the last 7, 30 and 90 days',
|
||||
properties: {
|
||||
last7: {
|
||||
type: 'number',
|
||||
description:
|
||||
'The number of active users in the last 7 days',
|
||||
example: 5,
|
||||
minimum: 0,
|
||||
},
|
||||
last30: {
|
||||
type: 'number',
|
||||
description:
|
||||
'The number of active users in the last 30 days',
|
||||
example: 10,
|
||||
minimum: 0,
|
||||
},
|
||||
last90: {
|
||||
type: 'number',
|
||||
description:
|
||||
'The number of active users in the last 90 days',
|
||||
example: 15,
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
featureToggles: {
|
||||
type: 'number',
|
||||
description: 'The number of feature-toggles this instance has',
|
||||
@ -124,6 +152,18 @@ export const instanceAdminStatsSchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
featureExports: {
|
||||
type: 'number',
|
||||
description: 'The number of export operations on this instance',
|
||||
example: 0,
|
||||
minimum: 0,
|
||||
},
|
||||
featureImports: {
|
||||
type: 'number',
|
||||
description: 'The number of import operations on this instance',
|
||||
example: 0,
|
||||
minimum: 0,
|
||||
},
|
||||
sum: {
|
||||
type: 'string',
|
||||
description:
|
||||
|
@ -108,6 +108,11 @@ class InstanceAdminController extends Controller {
|
||||
users: 10,
|
||||
versionEnterprise: '5.1.7',
|
||||
versionOSS: '5.1.7',
|
||||
activeUsers: {
|
||||
last90: 15,
|
||||
last30: 10,
|
||||
last7: 5,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ test('get snapshot should not call getStats', async () => {
|
||||
// subsequent calls to getStatsSnapshot don't call getStats
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const stats = instanceStatsService.getStatsSnapshot();
|
||||
expect(stats.clientApps).toStrictEqual([
|
||||
expect(stats?.clientApps).toStrictEqual([
|
||||
{ range: 'allTime', count: 0 },
|
||||
{ range: '30d', count: 0 },
|
||||
{ range: '7d', count: 0 },
|
||||
|
@ -12,7 +12,7 @@ import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||
import { IGroupStore } from '../types/stores/group-store';
|
||||
import { IProjectStore } from '../types/stores/project-store';
|
||||
import { IStrategyStore } from '../types/stores/strategy-store';
|
||||
import { IUserStore } from '../types/stores/user-store';
|
||||
import { IActiveUsers, IUserStore } from '../types/stores/user-store';
|
||||
import { ISegmentStore } from '../types/stores/segment-store';
|
||||
import { IRoleStore } from '../types/stores/role-store';
|
||||
import VersionService from './version-service';
|
||||
@ -43,6 +43,7 @@ export interface InstanceStats {
|
||||
SAMLenabled: boolean;
|
||||
OIDCenabled: boolean;
|
||||
clientApps: { range: TimeRange; count: number }[];
|
||||
activeUsers: IActiveUsers;
|
||||
}
|
||||
|
||||
export interface InstanceStatsSigned extends InstanceStats {
|
||||
@ -176,6 +177,7 @@ export class InstanceStatsService {
|
||||
const [
|
||||
featureToggles,
|
||||
users,
|
||||
activeUsers,
|
||||
projects,
|
||||
contextFields,
|
||||
groups,
|
||||
@ -193,6 +195,7 @@ export class InstanceStatsService {
|
||||
] = await Promise.all([
|
||||
this.getToggleCount(),
|
||||
this.userStore.count(),
|
||||
this.userStore.getActiveUsersCount(),
|
||||
this.projectStore.count(),
|
||||
this.contextFieldStore.count(),
|
||||
this.groupStore.count(),
|
||||
@ -215,6 +218,7 @@ export class InstanceStatsService {
|
||||
versionOSS: versionInfo.current.oss,
|
||||
versionEnterprise: versionInfo.current.enterprise,
|
||||
users,
|
||||
activeUsers,
|
||||
featureToggles,
|
||||
projects,
|
||||
contextFields,
|
||||
|
@ -30,7 +30,8 @@ export type IFlagKey =
|
||||
| 'newApplicationList'
|
||||
| 'integrationsRework'
|
||||
| 'multipleRoles'
|
||||
| 'doraMetrics';
|
||||
| 'doraMetrics'
|
||||
| 'instanceHealthDashboard';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -138,7 +139,6 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_NEW_APPLICATION_LIST,
|
||||
false,
|
||||
),
|
||||
doraMetrics: parseEnvVarBoolean(process.env.UNLEASH_DORA_METRICS, false),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -19,6 +19,12 @@ export interface IUserUpdateFields {
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface IActiveUsers {
|
||||
last7: number;
|
||||
last30: number;
|
||||
last90: number;
|
||||
}
|
||||
|
||||
export interface IUserStore extends Store<IUser, number> {
|
||||
update(id: number, fields: IUserUpdateFields): Promise<IUser>;
|
||||
insert(user: ICreateUser): Promise<IUser>;
|
||||
@ -32,4 +38,5 @@ export interface IUserStore extends Store<IUser, number> {
|
||||
incLoginAttempts(user: IUser): Promise<void>;
|
||||
successfullyLogin(user: IUser): Promise<void>;
|
||||
count(): Promise<number>;
|
||||
getActiveUsersCount(): Promise<IActiveUsers>;
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ process.nextTick(async () => {
|
||||
segmentChangeRequests: true,
|
||||
newApplicationList: true,
|
||||
doraMetrics: true,
|
||||
instanceHealthDashboard: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
21
src/test/fixtures/fake-user-store.ts
vendored
21
src/test/fixtures/fake-user-store.ts
vendored
@ -1,5 +1,6 @@
|
||||
import User, { IUser } from '../../lib/types/user';
|
||||
import {
|
||||
IActiveUsers,
|
||||
ICreateUser,
|
||||
IUserLookup,
|
||||
IUserStore,
|
||||
@ -46,6 +47,26 @@ class UserStoreMock implements IUserStore {
|
||||
return this.data.find((u) => u.id === key);
|
||||
}
|
||||
|
||||
async getActiveUsersCount(): Promise<IActiveUsers> {
|
||||
return Promise.resolve({
|
||||
last7: this.data.filter(
|
||||
(u) =>
|
||||
u.seenAt &&
|
||||
u.seenAt > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
).length,
|
||||
last30: this.data.filter(
|
||||
(u) =>
|
||||
u.seenAt &&
|
||||
u.seenAt > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||
).length,
|
||||
last90: this.data.filter(
|
||||
(u) =>
|
||||
u.seenAt &&
|
||||
u.seenAt > new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
|
||||
).length,
|
||||
});
|
||||
}
|
||||
|
||||
async insert(user: User): Promise<User> {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
user.id = this.idSeq;
|
||||
|
Loading…
Reference in New Issue
Block a user