diff --git a/jsapp/js/UniversalTable/index.tsx b/jsapp/js/UniversalTable/index.tsx index 1b0745664e..f526ad3c63 100644 --- a/jsapp/js/UniversalTable/index.tsx +++ b/jsapp/js/UniversalTable/index.tsx @@ -20,24 +20,36 @@ export interface PaginatedListResponseData { results: Datum[] } -export type PaginatedListResponse = PaginatedListResponse.Ok | PaginatedListResponse.NotFound +export type PaginatedListResponse = + | PaginatedListResponse.Ok + | PaginatedListResponse.Unauthorized + | PaginatedListResponse.Forbidden + | PaginatedListResponse.NotFound export namespace PaginatedListResponse { export interface Ok { data: PaginatedListResponseData status: 200 } + export interface Unauthorized { + data: unknown + status: 401 + } + export interface Forbidden { + data: unknown + status: 403 + } export interface NotFound { data: unknown status: 404 } } -interface UniversalTableProps { +interface UniversalTableProps { // Below are props from `UniversalTable` that should come from the parent // component (these are kind of "configuration" props). The other // `UniversalTable` props are being handled here internally. columns: Array> - queryResult: UseQueryResult> + queryResult: UseQueryResult, TError> pagination: Pagination setPagination: (pagination: Pagination) => unknown } @@ -74,12 +86,12 @@ export const DEFAULT_PAGE_SIZE = PAGE_SIZES[0] * } * ``` */ -export default function UniversalTable({ +export default function UniversalTable({ columns, pagination, queryResult, setPagination, -}: UniversalTableProps) { +}: UniversalTableProps) { const availablePages = useMemo( () => (queryResult.data?.status === 200 ? Math.ceil(queryResult.data?.data?.count / pagination.limit) : 0), [pagination.limit, queryResult.data?.status, (queryResult.data?.data as PaginatedListResponseData)?.count], diff --git a/jsapp/js/account/security/accessLogs/accessLogs.query.ts b/jsapp/js/account/security/accessLogs/accessLogs.query.ts deleted file mode 100644 index 88b05e1d2e..0000000000 --- a/jsapp/js/account/security/accessLogs/accessLogs.query.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { fetchGet, fetchPost } from '#/api' -import { endpoints } from '#/api.endpoints' -import type { FailResponse, PaginatedResponse } from '#/dataInterface' - -export interface AccessLog { - /** User URL */ - user: string - user_uid: string - /** Date string */ - date_created: string - username: string - metadata: { - auth_type: 'digest' | 'submission-group' | string - // Both `source` and `ip_address` appear only for `digest` type - /** E.g. "Firefox (Ubuntu)" */ - source?: string - ip_address?: string - } - /** For `submission-group` type, here is the number of submisssions. */ - count: number -} - -export async function getAccessLogs(limit: number, offset: number) { - const params = new URLSearchParams({ - limit: limit.toString(), - offset: offset.toString(), - }) - // Note: little crust ahead of time to make a simpler transition to generated react-query helpers. - return { - status: 200 as const, - data: await fetchGet>(endpoints.ACCESS_LOGS_URL + '?' + params, { - errorMessageDisplay: t('There was an error getting the list.'), - }), - } -} - -/** - * Starts the exporting process of the access logs. - * @returns {Promise} A promise that starts the export. - */ -export const startAccessLogsExport = () => - fetchPost(endpoints.ACCESS_LOGS_EXPORT_URL, {}, { notifyAboutError: false }).catch((error) => { - const failResponse: FailResponse = { - status: 500, - statusText: error.message || t('An error occurred while exporting the logs'), - } - throw failResponse - }) diff --git a/jsapp/js/account/security/accessLogs/accessLogsSection.component.tsx b/jsapp/js/account/security/accessLogs/accessLogsSection.component.tsx index de83670d1d..a2e9dc5d96 100644 --- a/jsapp/js/account/security/accessLogs/accessLogsSection.component.tsx +++ b/jsapp/js/account/security/accessLogs/accessLogsSection.component.tsx @@ -1,34 +1,52 @@ import React, { useState } from 'react' -import { keepPreviousData, useQuery } from '@tanstack/react-query' +import { keepPreviousData } from '@tanstack/react-query' import UniversalTable, { DEFAULT_PAGE_SIZE } from '#/UniversalTable' import securityStyles from '#/account/security/securityRoute.module.scss' +import type { AccessLogResponse } from '#/api/models/accessLogResponse' +import type { ErrorDetail } from '#/api/models/errorDetail' +import { + getAccessLogsMeListQueryKey, + useAccessLogsMeExportCreate, + useAccessLogsMeList, +} from '#/api/react-query/logging' import Button from '#/components/common/button' import ExportToEmailButton from '#/components/exportToEmailButton/exportToEmailButton.component' -import { QueryKeys } from '#/query/queryKeys' +import type { FailResponse } from '#/dataInterface' import sessionStore from '#/stores/session' import { formatTime } from '#/utils' -import { type AccessLog, getAccessLogs, startAccessLogsExport } from './accessLogs.query' export default function AccessLogsSection() { const [pagination, setPagination] = useState({ limit: DEFAULT_PAGE_SIZE, offset: 0, }) - const queryResult = useQuery({ - queryKey: [QueryKeys.accessLogs, pagination.limit, pagination.offset], - queryFn: () => getAccessLogs(pagination.limit, pagination.offset), - placeholderData: keepPreviousData, - // We might want to improve this in future, for now let's not retry - retry: false, - // The `refetchOnWindowFocus` option is `true` by default, I'm setting it - // here so we don't forget about it. - refetchOnWindowFocus: true, + const queryResult = useAccessLogsMeList(pagination, { + query: { + queryKey: getAccessLogsMeListQueryKey(pagination), + placeholderData: keepPreviousData, + }, + }) + const accessLogsMeExport = useAccessLogsMeExportCreate({ + request: { + notifyAboutError: false, + }, }) function logOutAllSessions() { sessionStore.logOutAll() } + const handleStartExport = async () => { + try { + await accessLogsMeExport.mutateAsync() + } catch (error) { + const failResponse: FailResponse = { + status: 500, + statusText: (error as Error).message || t('An error occurred while exporting the logs'), + } + throw failResponse + } + } return ( <> @@ -43,11 +61,11 @@ export default function AccessLogsSection() { startIcon='logout' /> - + - + pagination={pagination} setPagination={setPagination} queryResult={queryResult} @@ -57,7 +75,7 @@ export default function AccessLogsSection() { { key: 'metadata.source', label: t('Source'), - cellFormatter: (log: AccessLog) => { + cellFormatter: (log: AccessLogResponse) => { if (log.metadata.auth_type === 'submission-group') { return t('Data Submissions (##count##)').replace('##count##', String(log.count)) } else { @@ -68,7 +86,7 @@ export default function AccessLogsSection() { { key: 'date_created', label: t('Last activity'), - cellFormatter: (log: AccessLog) => formatTime(log.date_created), + cellFormatter: (log: AccessLogResponse) => formatTime(log.date_created), }, { key: 'metadata.ip_address', label: t('IP Address') }, ]} diff --git a/jsapp/js/api.endpoints.ts b/jsapp/js/api.endpoints.ts index 902ccf11b4..e7d2c7e9f6 100644 --- a/jsapp/js/api.endpoints.ts +++ b/jsapp/js/api.endpoints.ts @@ -26,8 +26,6 @@ export const endpoints = { PROJECT_HISTORY_LOGS: '/api/v2/project-history-logs/', /** Expected parameters: price_id and subscription_id **/ CHANGE_PLAN_URL: '/api/v2/stripe/change-plan', - ACCESS_LOGS_URL: '/api/v2/access-logs/me', - ACCESS_LOGS_EXPORT_URL: '/api/v2/access-logs/me/export/', LOGOUT_ALL: '/logout-all/', LANGUAGES_LIST_URL: '/api/v2/languages/', LANGUAGE_DETAIL_URL: '/api/v2/languages/:language_id/', diff --git a/jsapp/js/query/queryKeys.ts b/jsapp/js/query/queryKeys.ts index 7d8e0a98fa..a40335738a 100644 --- a/jsapp/js/query/queryKeys.ts +++ b/jsapp/js/query/queryKeys.ts @@ -7,7 +7,6 @@ * Keep keys sorted alphabetically. */ export enum QueryKeys { - accessLogs = 'accessLogs', activityLogs = 'activityLogs', activityLogsFilter = 'activityLogsFilter', organization = 'organization',