Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/statgpt-admin-frontend/src/app/audit-logs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SIGN_IN_LINK } from '@/src/constants/auth';
import { getIsInvalidSession, getUserToken } from '@/src/utils/auth/get-token';
import { getIsEnableAuthToggle } from '@/src/utils/get-auth-toggle';
import { AuditLogsListView } from '@/src/components/AuditLogs/AuditLogsListView';
import { auditLogsApi } from '../api/api';

export const dynamic = 'force-dynamic';

Expand All @@ -16,5 +17,7 @@ export default async function Page() {
return redirect(SIGN_IN_LINK);
}

return <AuditLogsListView />;
const enums = await auditLogsApi.getEnumValues(token);

return <AuditLogsListView enums={enums} />;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import { useCallback, useEffect, useRef, useState } from 'react';
import { ListView } from '@/src/components/ListView/ListView';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Menu } from '@/src/constants/menu';
import type {
FetchRowsArgs,
Expand All @@ -10,18 +9,24 @@ import type {
import type {
AuditLog,
AuditLogDetails,
AuditLogEnumValues,
AuditLogRequestModel,
} from '@/src/models/audit-log';
import { AUDIT_LOGS_COLUMNS } from '@/src/constants/columns/grid-columns';
import { AuditLogsHeader } from './AuditLogsHeader';
import type { RequestData } from '@/src/models/request-data';
import { sendGetRequest } from '@/src/server/api';
import { DEFAULT_GRID_PAGE_SIZE } from '@/src/constants/columns/grid';
import { useAuditLogFiltersInUrl } from '@/src/hooks/use-audit-logs-filters-in-url';
import { auditLogRequestToQueryString } from '@/src/utils/audit-logs';
import { getTextEquals } from '@/src/utils/client/grid';
import { getEnumFilterValue, getTextEquals } from '@/src/utils/client/grid';
import { ListContent } from '../ListView/ListContent/ListContent';
import { getAuditLogsColumns } from '@/src/constants/columns/audit-logs';

export function AuditLogsListView() {
interface AuditLogsListViewProps {
enums?: AuditLogEnumValues | null;
}

export function AuditLogsListView({ enums }: AuditLogsListViewProps) {
const [totalCount, setTotalCount] = useState<number | undefined>(undefined);

const { filters, queryKey } = useAuditLogFiltersInUrl();
Expand All @@ -37,8 +42,9 @@ export function AuditLogsListView() {
async (args: FetchRowsArgs): Promise<FetchRowsResult<AuditLog>> => {
const { created_at_from, created_at_to } = filtersRef.current;

const entity_type = getTextEquals(args.filterModel, 'entity_type');
const action_type = getTextEquals(args.filterModel, 'action_type');
const entity_type = getEnumFilterValue(args.filterModel, 'entity_type');
const action_type = getEnumFilterValue(args.filterModel, 'action_type');

const entity_id = getTextEquals(args.filterModel, 'entity_id');

const request: AuditLogRequestModel = {
Expand Down Expand Up @@ -72,21 +78,24 @@ export function AuditLogsListView() {
[],
);

const columns = useMemo(() => getAuditLogsColumns({ enums }), [enums]);

return (
<ListView<AuditLog>
colDefs={AUDIT_LOGS_COLUMNS}
customHeader={
<AuditLogsHeader
onRefresh={() => setRefreshToken((x) => x + 1)}
count={totalCount}
/>
}
emptyDataTitle="No audit logs"
menuItem={Menu.AUDIT_LOGS}
fetchRows={fetchRows}
pageSize={DEFAULT_GRID_PAGE_SIZE}
queryKey={queryKey}
refreshToken={refreshToken}
/>
<div className="flex flex-col h-full rounded bg-layer-2 common-paddings">
<AuditLogsHeader
onRefresh={() => setRefreshToken((x) => x + 1)}
count={totalCount}
/>
<ListContent<AuditLog>
colDefs={columns}
emptyDataTitle="No audit logs"
menuItem={Menu.AUDIT_LOGS}
fetchRows={fetchRows}
pageSize={DEFAULT_GRID_PAGE_SIZE}
queryKey={queryKey}
refreshToken={refreshToken}
withHeader={false}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,11 @@ export const TimePeriodDropdown = ({
preventOpenOnFocus
/>

<Button cssClass="primary" title="Apply" onClick={applyCustom} />
<Button
cssClass="primary w-fit"
title="Apply"
onClick={applyCustom}
/>
</div>
<div className="w-full border border-primary my-1" />
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client';

import { EnumSelectFilterModel } from '@/src/models/grid';
import { normalizeEnumValues } from '@/src/utils/client/grid';
import React, { useMemo } from 'react';

export interface EnumSelectFilterProps {
model: EnumSelectFilterModel;
onModelChange: (model: EnumSelectFilterModel) => void;
values: readonly string[];
allLabel?: string;
formatValue?: (v: string) => string;
colDef?: { headerName?: string; field?: string };
}

export function EnumSelectFilter({
model,
onModelChange,
values,
allLabel = 'All',
formatValue,
colDef,
}: EnumSelectFilterProps) {
const options = useMemo(() => normalizeEnumValues(values), [values]);
const selected = model?.value ?? '';

return (
<div className="flex flex-col gap-2 p-2">
<div className="text-xs text-secondary">
{colDef?.headerName ?? colDef?.field ?? 'Filter'}
</div>

<select
className="w-full rounded border border-primary bg-layer-2 px-2 py-1 text-sm text-primary outline-none"
value={selected}
onChange={(e) => {
const next = e.target.value;
onModelChange(next ? { value: next } : null);
}}
>
<option value="">{allLabel}</option>

{options.map((v) => (
<option key={v} value={v}>
{formatValue ? formatValue(v) : v}
</option>
))}
</select>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';

import { EnumSelectFilterModel } from '@/src/models/grid';
import { normalizeEnumValues } from '@/src/utils/client/grid';
import React, { useMemo } from 'react';

export interface EnumSelectFloatingFilterProps {
model: EnumSelectFilterModel;
onModelChange: (model: EnumSelectFilterModel) => void;
values: readonly string[];
allLabel?: string;
formatValue?: (v: string) => string;
}

export function EnumSelectFloatingFilter({
model,
onModelChange,
values,
allLabel = 'All',
formatValue,
}: EnumSelectFloatingFilterProps) {
const options = useMemo(() => normalizeEnumValues(values), [values]);
const selected = model?.value ?? '';

return (
<select
className="w-full rounded border border-primary bg-layer-1 my-[1px] px-1 text-xs text-primary outline-none"
value={selected}
onChange={(e) => {
const next = e.target.value;
onModelChange(next ? { value: next } : null);
}}
>
<option value="">{allLabel}</option>
{options.map((v) => (
<option key={v} value={v}>
{formatValue ? formatValue(v) : v}
</option>
))}
</select>
);
}
63 changes: 44 additions & 19 deletions apps/statgpt-admin-frontend/src/components/GridView/GridView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
'use client';

import React, {
memo,
useEffect,
useMemo,
useRef,
useState,
useCallback,
} from 'react';
import {
AllCommunityModule,
ColDef,
Expand All @@ -13,7 +21,6 @@ import {
themeBalham,
} from 'ag-grid-community';
import { AgGridReact } from 'ag-grid-react';
import { useEffect, useMemo, useRef, useState } from 'react';

import { ActionColumn } from '@/src/components/ListView/ActionColumn/ActionColumn';
import { ACTION_COLUMN_CELL_RENDERER_KEY } from '@/src/constants/columns/action';
Expand Down Expand Up @@ -53,7 +60,7 @@ ModuleRegistry.registerModules([AllCommunityModule]);
const GRID_CUSTOM_COMPONENT = {
[ACTION_COLUMN_CELL_RENDERER_KEY]: ActionColumn,
[AUDIT_LOG_DETAILS_CELL_RENDERER_KEY]: AuditLogDetailsCellRenderer,
};
} as const;

const GRID_THEME_COLORS = {
accentColor: 'var(--controls-bg-accent, #5C8DEA)',
Expand All @@ -73,9 +80,9 @@ const GRID_THEME_COLORS = {
fontFamily: {
googleFont: 'var(--theme-font, var(--font-inter))',
},
};
} as const;

export function GridView<T = BaseEntity>({
function GridViewInner<T = BaseEntity>({
data,
colDefs,
emptyDataTitle,
Expand All @@ -97,6 +104,18 @@ export function GridView<T = BaseEntity>({

const isInfinite = typeof fetchRows === 'function';

const gridTheme = useMemo(() => {
return themeBalham.withPart(colorSchemeDark).withParams(GRID_THEME_COLORS);
}, []);

const defaultColDef = useMemo<ColDef>(() => {
return {
floatingFilter: true,
tooltipValueGetter: (p: ITooltipParams) =>
p.data?.[(p.colDef as ColDef)?.field || ''],
};
}, []);

useEffect(() => {
if (!api || !isInfinite) return;
api.purgeInfiniteCache();
Expand Down Expand Up @@ -147,40 +166,46 @@ export function GridView<T = BaseEntity>({

const shouldShowEmpty = !isInfinite && (!data || data.length === 0);

const onGridReady = useCallback((e: any) => {
setApi(e.api);
}, []);

const lastDatasourceRef = useRef<IDatasource | undefined>(undefined);
useEffect(() => {
if (!api || !isInfinite) return;
if (!datasource) return;

if (lastDatasourceRef.current !== datasource) {
lastDatasourceRef.current = datasource;
api.setGridOption('datasource', datasource);
}
}, [api, datasource, isInfinite]);

return shouldShowEmpty ? (
<EmptyState title={emptyDataTitle} />
) : (
<div className="ag-theme-balham-dark h-full">
<AgGridReact
columnDefs={colDefs}
theme={themeBalham
.withPart(colorSchemeDark)
.withParams({ ...GRID_THEME_COLORS })}
theme={gridTheme}
headerHeight={28}
rowHeight={32}
suppressCellFocus={true}
components={GRID_CUSTOM_COMPONENT}
onGridReady={(e) => {
setApi(e.api);

if (datasource) {
e.api.setGridOption('datasource', datasource);
}
}}
onGridReady={onGridReady}
tooltipShowDelay={500}
defaultColDef={{
floatingFilter: true,
tooltipValueGetter: (p: ITooltipParams) =>
p.data?.[(p.colDef as ColDef)?.field || ''],
}}
defaultColDef={defaultColDef}
onGridSizeChanged={(e) => e.api.sizeColumnsToFit()}
rowModelType={isInfinite ? 'infinite' : undefined}
rowData={isInfinite ? undefined : (data ?? [])}
cacheBlockSize={isInfinite ? pageSize : undefined}
maxBlocksInCache={isInfinite ? 5 : undefined}
infiniteInitialRowCount={isInfinite ? pageSize : undefined}
enableFilterHandlers={true}
{...additionalOptions}
/>
</div>
);
}

export const GridView = memo(GridViewInner) as typeof GridViewInner;
Loading
Loading