diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx new file mode 100644 index 0000000000000..72b6d7fa46558 --- /dev/null +++ b/site/src/components/Filter/UserFilter.tsx @@ -0,0 +1,100 @@ +import { useMe } from "hooks" +import { BaseOption } from "./options" +import { getUsers } from "api/api" +import { UseFilterMenuOptions, useFilterMenu } from "./menu" +import { FilterSearchMenu, OptionItem } from "./filter" +import { UserAvatar } from "components/UserAvatar/UserAvatar" + +export type UserOption = BaseOption & { + avatarUrl?: string +} + +export const useUserFilterMenu = ({ + value, + onChange, + enabled, +}: Pick< + UseFilterMenuOptions, + "value" | "onChange" | "enabled" +>) => { + const me = useMe() + + const addMeAsFirstOption = (options: UserOption[]) => { + options = options.filter((option) => option.value !== me.username) + return [ + { label: me.username, value: me.username, avatarUrl: me.avatar_url }, + ...options, + ] + } + + return useFilterMenu({ + onChange, + enabled, + value, + id: "owner", + getSelectedOption: async () => { + const usersRes = await getUsers({ q: value, limit: 1 }) + const firstUser = usersRes.users.at(0) + if (firstUser && firstUser.username === value) { + return { + label: firstUser.username, + value: firstUser.username, + avatarUrl: firstUser.avatar_url, + } + } + return null + }, + getOptions: async (query) => { + const usersRes = await getUsers({ q: query, limit: 25 }) + let options: UserOption[] = usersRes.users.map((user) => ({ + label: user.username, + value: user.username, + avatarUrl: user.avatar_url, + })) + options = addMeAsFirstOption(options) + return options + }, + }) +} + +export type UserFilterMenu = ReturnType + +export const UserMenu = (menu: UserFilterMenu) => { + return ( + + ) : ( + "All users" + ) + } + > + {(itemProps) => } + + ) +} + +const UserOptionItem = ({ + option, + isSelected, +}: { + option: UserOption + isSelected?: boolean +}) => { + return ( + + } + /> + ) +} diff --git a/site/src/components/Filter/filter.tsx b/site/src/components/Filter/filter.tsx index 3e85c0dd42c71..2999cb8a1f046 100644 --- a/site/src/components/Filter/filter.tsx +++ b/site/src/components/Filter/filter.tsx @@ -175,6 +175,7 @@ export const Filter = ({ } size="small" InputProps={{ + "aria-label": "Filter", name: "query", placeholder: "Search...", value: searchQuery, diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx new file mode 100644 index 0000000000000..d948d4472998c --- /dev/null +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -0,0 +1,167 @@ +import { AuditActions, ResourceTypes } from "api/typesGenerated" +import { UserFilterMenu, UserMenu } from "components/Filter/UserFilter" +import { + Filter, + FilterMenu, + MenuSkeleton, + OptionItem, + SearchFieldSkeleton, + useFilter, +} from "components/Filter/filter" +import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu" +import { BaseOption } from "components/Filter/options" +import capitalize from "lodash/capitalize" + +const PRESET_FILTERS = [ + { + query: "resource_type:workspace action:create", + name: "Created workspaces", + }, + { query: "resource_type:template action:create", name: "Added templates" }, + { query: "resource_type:user action:delete", name: "Deleted users" }, + { + query: "resource_type:workspace_build action:start build_reason:initiator", + name: "Builds started by a user", + }, + { + query: "resource_type:api_key action:login", + name: "User logins", + }, +] + +export const AuditFilter = ({ + filter, + error, + menus, +}: { + filter: ReturnType + error?: unknown + menus: { + user: UserFilterMenu + action: ActionFilterMenu + resourceType: ResourceTypeFilterMenu + } +}) => { + return ( + + + + + + } + skeleton={ + <> + + + + + + } + /> + ) +} + +export const useActionFilterMenu = ({ + value, + onChange, +}: Pick, "value" | "onChange">) => { + const actionOptions: BaseOption[] = AuditActions.map((action) => ({ + value: action, + label: capitalize(action), + })) + return useFilterMenu({ + onChange, + value, + id: "status", + getSelectedOption: async () => + actionOptions.find((option) => option.value === value) ?? null, + getOptions: async () => actionOptions, + }) +} + +export type ActionFilterMenu = ReturnType + +const ActionMenu = (menu: ActionFilterMenu) => { + return ( + + ) : ( + "All actions" + ) + } + > + {(itemProps) => } + + ) +} + +export const useResourceTypeFilterMenu = ({ + value, + onChange, +}: Pick, "value" | "onChange">) => { + const actionOptions: BaseOption[] = ResourceTypes.map((type) => { + let label = capitalize(type) + + if (type === "api_key") { + label = "API Key" + } + + if (type === "git_ssh_key") { + label = "Git SSH Key" + } + + if (type === "template_version") { + label = "Template Version" + } + + if (type === "workspace_build") { + label = "Workspace Build" + } + + return { + value: type, + label, + } + }) + return useFilterMenu({ + onChange, + value, + id: "resourceType", + getSelectedOption: async () => + actionOptions.find((option) => option.value === value) ?? null, + getOptions: async () => actionOptions, + }) +} + +export type ResourceTypeFilterMenu = ReturnType< + typeof useResourceTypeFilterMenu +> + +const ResourceTypeMenu = (menu: ResourceTypeFilterMenu) => { + return ( + + ) : ( + "All resource types" + ) + } + > + {(itemProps) => } + + ) +} diff --git a/site/src/pages/AuditPage/AuditPage.test.tsx b/site/src/pages/AuditPage/AuditPage.test.tsx index 32d2e868046f9..32bd5d53f6fc2 100644 --- a/site/src/pages/AuditPage/AuditPage.test.tsx +++ b/site/src/pages/AuditPage/AuditPage.test.tsx @@ -68,18 +68,6 @@ describe("AuditPage", () => { }) describe("Filtering", () => { - it("filters by typing", async () => { - await renderPage() - await screen.findByText("updated", { exact: false }) - - const filterField = screen.getByLabelText("Filter") - const query = "resource_type:workspace action:create" - await userEvent.type(filterField, query) - await screen.findByText("created", { exact: false }) - const editWorkspace = screen.queryByText("updated", { exact: false }) - expect(editWorkspace).not.toBeInTheDocument() - }) - it("filters by URL", async () => { const getAuditLogsSpy = jest .spyOn(API, "getAuditLogs") @@ -88,13 +76,14 @@ describe("AuditPage", () => { const query = "resource_type:workspace action:create" await renderPage({ filter: query }) - expect(getAuditLogsSpy).toBeCalledWith({ limit: 25, offset: 0, q: query }) + expect(getAuditLogsSpy).toBeCalledWith({ limit: 25, offset: 1, q: query }) }) it("resets page to 1 when filter is changed", async () => { await renderPage({ page: 2 }) const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs") + getAuditLogsSpy.mockClear() const filterField = screen.getByLabelText("Filter") const query = "resource_type:workspace action:create" @@ -103,7 +92,7 @@ describe("AuditPage", () => { await waitFor(() => expect(getAuditLogsSpy).toBeCalledWith({ limit: 25, - offset: 0, + offset: 1, q: query, }), ) diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index 702f3390ddf2e..b5932443f8792 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -1,34 +1,63 @@ -import { useMachine } from "@xstate/react" -import { - getPaginationContext, - nonInitialPage, -} from "components/PaginationWidget/utils" +import { nonInitialPage } from "components/PaginationWidget/utils" import { useFeatureVisibility } from "hooks/useFeatureVisibility" import { FC } from "react" import { Helmet } from "react-helmet-async" import { useSearchParams } from "react-router-dom" import { pageTitle } from "utils/page" -import { auditMachine } from "xServices/audit/auditXService" -import { PaginationMachineRef } from "xServices/pagination/paginationXService" import { AuditPageView } from "./AuditPageView" +import { useUserFilterMenu } from "components/Filter/UserFilter" +import { useFilter } from "components/Filter/filter" +import { useDashboard } from "components/Dashboard/DashboardProvider" +import { usePagination } from "hooks" +import { useQuery } from "@tanstack/react-query" +import { getAuditLogs } from "api/api" +import { useActionFilterMenu, useResourceTypeFilterMenu } from "./AuditFilter" const AuditPage: FC = () => { - const [searchParams, setSearchParams] = useSearchParams() - const filter = searchParams.get("filter") ?? "" - const [auditState, auditSend] = useMachine(auditMachine, { - context: { - filter, - paginationContext: getPaginationContext(searchParams), - }, - actions: { - updateURL: (context, event) => - setSearchParams({ page: event.page, filter: context.filter }), + const dashboard = useDashboard() + const searchParamsResult = useSearchParams() + const pagination = usePagination({ searchParamsResult }) + const filter = useFilter({ + searchParamsResult, + onUpdate: () => { + pagination.goToPage(1) }, }) - - const { auditLogs, count, apiError } = auditState.context - const paginationRef = auditState.context.paginationRef as PaginationMachineRef + const userMenu = useUserFilterMenu({ + value: filter.values.username, + onChange: (option) => + filter.update({ + ...filter.values, + username: option?.value, + }), + }) + const actionMenu = useActionFilterMenu({ + value: filter.values.action, + onChange: (option) => + filter.update({ + ...filter.values, + action: option?.value, + }), + }) + const resourceTypeMenu = useResourceTypeFilterMenu({ + value: filter.values["resource_type"], + onChange: (option) => + filter.update({ + ...filter.values, + resource_type: option?.value, + }), + }) const { audit_log: isAuditLogVisible } = useFeatureVisibility() + const { data, error } = useQuery({ + queryKey: ["auditLogs", filter.query, pagination.page], + queryFn: () => { + return getAuditLogs({ + offset: pagination.page, + limit: 25, + q: filter.query, + }) + }, + }) return ( <> @@ -36,16 +65,29 @@ const AuditPage: FC = () => { {pageTitle("Audit")} { - auditSend("FILTER", { filter }) - }} - paginationRef={paginationRef} - isNonInitialPage={nonInitialPage(searchParams)} + auditLogs={data?.audit_logs} + count={data?.count} + page={pagination.page} + limit={pagination.limit} + onPageChange={pagination.goToPage} + isNonInitialPage={nonInitialPage(searchParamsResult[0])} isAuditLogVisible={isAuditLogVisible} - error={apiError} + error={error} + filterProps={ + dashboard.experiments.includes("workspace_filter") + ? { + filter, + menus: { + user: userMenu, + action: actionMenu, + resourceType: resourceTypeMenu, + }, + } + : { + filter: filter.query, + onFilter: filter.update, + } + } /> ) diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index c6a01fa5630f3..94f00ab54d8c1 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -1,51 +1,91 @@ -import { ComponentMeta, Story } from "@storybook/react" -import { createPaginationRef } from "components/PaginationWidget/utils" -import { MockAuditLog, MockAuditLog2 } from "testHelpers/entities" -import { AuditPageView, AuditPageViewProps } from "./AuditPageView" +/* eslint-disable eslint-comments/disable-enable-pair -- ignore */ +/* eslint-disable @typescript-eslint/no-explicit-any -- We don't care about any here */ +import { Meta, StoryObj } from "@storybook/react" +import { MockAuditLog, MockAuditLog2, MockUser } from "testHelpers/entities" +import { AuditPageView } from "./AuditPageView" +import { action } from "@storybook/addon-actions" +import { WorkspacesPageView } from "pages/WorkspacesPage/WorkspacesPageView" +import { ComponentProps } from "react" -export default { +const mockMenu = { + initialOption: undefined, + isInitializing: false, + isSearching: false, + query: "", + searchOptions: [], + selectedOption: undefined, + selectOption: action("selectOption"), + setQuery: action("updateQuery"), +} + +const defaultFilterProps = { + filter: { + query: `owner:me`, + update: () => action("update"), + debounceUpdate: action("debounce") as any, + used: false, + values: { + username: MockUser.username, + action: undefined, + resource_type: undefined, + }, + }, + menus: { + user: mockMenu, + action: mockMenu, + resourceType: mockMenu, + }, +} as ComponentProps["filterProps"] + +const meta: Meta = { title: "pages/AuditPageView", component: AuditPageView, args: { auditLogs: [MockAuditLog, MockAuditLog2], count: 1000, - paginationRef: createPaginationRef({ page: 1, limit: 25 }), + page: 1, + limit: 25, isAuditLogVisible: true, + filterProps: defaultFilterProps, }, -} as ComponentMeta +} -const Template: Story = (args) => ( - -) +export default meta +type Story = StoryObj -export const AuditPage = Template.bind({}) +export const AuditPage: Story = {} -export const Loading = Template.bind({}) -Loading.args = { - auditLogs: undefined, - count: undefined, - isNonInitialPage: false, +export const Loading = { + args: { + auditLogs: undefined, + count: undefined, + isNonInitialPage: false, + }, } -export const EmptyPage = Template.bind({}) -EmptyPage.args = { - auditLogs: [], - isNonInitialPage: true, +export const EmptyPage = { + args: { + auditLogs: [], + isNonInitialPage: true, + }, } -export const NoLogs = Template.bind({}) -NoLogs.args = { - auditLogs: [], - count: 0, - isNonInitialPage: false, +export const NoLogs = { + args: { + auditLogs: [], + count: 0, + isNonInitialPage: false, + }, } -export const NotVisible = Template.bind({}) -NotVisible.args = { - isAuditLogVisible: false, +export const NotVisible = { + args: { + isAuditLogVisible: false, + }, } -export const AuditPageSmallViewport = Template.bind({}) -AuditPageSmallViewport.parameters = { - chromatic: { viewports: [600] }, +export const AuditPageSmallViewport = { + parameters: { + chromatic: { viewports: [600] }, + }, } diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index d2180067df578..559faefb8a0ad 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -13,16 +13,17 @@ import { PageHeaderSubtitle, PageHeaderTitle, } from "components/PageHeader/PageHeader" -import { PaginationWidget } from "components/PaginationWidget/PaginationWidget" import { SearchBarWithFilter } from "components/SearchBarWithFilter/SearchBarWithFilter" import { Stack } from "components/Stack/Stack" import { TableLoader } from "components/TableLoader/TableLoader" import { Timeline } from "components/Timeline/Timeline" import { AuditHelpTooltip } from "components/Tooltips" -import { FC } from "react" +import { ComponentProps, FC } from "react" import { useTranslation } from "react-i18next" -import { PaginationMachineRef } from "xServices/pagination/paginationXService" import { AuditPaywall } from "./AuditPaywall" +import { AuditFilter } from "./AuditFilter" +import { PaginationStatus } from "components/PaginationStatus/PaginationStatus" +import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase" export const Language = { title: "Audit", @@ -49,23 +50,27 @@ const presetFilters = [ export interface AuditPageViewProps { auditLogs?: AuditLog[] count?: number - filter: string - onFilter: (filter: string) => void - paginationRef: PaginationMachineRef + page: number + limit: number + onPageChange: (page: number) => void isNonInitialPage: boolean isAuditLogVisible: boolean error?: Error | unknown + filterProps: + | ComponentProps + | ComponentProps } export const AuditPageView: FC = ({ auditLogs, count, - filter, - onFilter, - paginationRef, + page, + limit, + onPageChange, isNonInitialPage, isAuditLogVisible, error, + filterProps, }) => { const { t } = useTranslation("auditLog") @@ -86,12 +91,22 @@ export const AuditPageView: FC = ({ - + ) : ( + + )} + + @@ -143,7 +158,14 @@ export const AuditPageView: FC = ({ - + {count !== undefined && ( + + )} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 40f566a69c92e..3114d7f8118b8 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -5,14 +5,11 @@ import { pageTitle } from "utils/page" import { useWorkspacesData, useWorkspaceUpdate } from "./data" import { WorkspacesPageView } from "./WorkspacesPageView" import { useOrganizationId, usePermissions } from "hooks" -import { - useUserFilterMenu, - useTemplateFilterMenu, - useStatusFilterMenu, -} from "./filter/menus" +import { useTemplateFilterMenu, useStatusFilterMenu } from "./filter/menus" import { useSearchParams } from "react-router-dom" import { useDashboard } from "components/Dashboard/DashboardProvider" import { useFilter } from "components/Filter/filter" +import { useUserFilterMenu } from "components/Filter/UserFilter" const WorkspacesPage: FC = () => { const orgId = useOrganizationId() @@ -67,6 +64,7 @@ const WorkspacesPage: FC = () => { count={data?.count} page={pagination.page} limit={pagination.limit} + onPageChange={pagination.goToPage} filterProps={{ filter, menus: { @@ -75,7 +73,6 @@ const WorkspacesPage: FC = () => { status: statusMenu, }, }} - onPageChange={pagination.goToPage} onUpdateWorkspace={(workspace) => { updateWorkspace.mutate(workspace) }} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index ad49ac05590f0..297f0c40f9a0b 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -50,9 +50,9 @@ export interface WorkspacesPageViewProps { workspaces?: Workspace[] count?: number useNewFilter?: boolean + filterProps: ComponentProps page: number limit: number - filterProps: ComponentProps onPageChange: (page: number) => void onUpdateWorkspace: (workspace: Workspace) => void } diff --git a/site/src/pages/WorkspacesPage/filter/filter.tsx b/site/src/pages/WorkspacesPage/filter/filter.tsx index a6f6994278732..d5f8d6b8b7fe7 100644 --- a/site/src/pages/WorkspacesPage/filter/filter.tsx +++ b/site/src/pages/WorkspacesPage/filter/filter.tsx @@ -1,10 +1,9 @@ import { FC } from "react" import Box from "@mui/material/Box" -import { UserAvatar } from "components/UserAvatar/UserAvatar" import { Avatar, AvatarProps } from "components/Avatar/Avatar" import { Palette, PaletteColor } from "@mui/material/styles" -import { UserFilterMenu, TemplateFilterMenu, StatusFilterMenu } from "./menus" -import { UserOption, TemplateOption, StatusOption } from "./options" +import { TemplateFilterMenu, StatusFilterMenu } from "./menus" +import { TemplateOption, StatusOption } from "./options" import { Filter, FilterMenu, @@ -14,6 +13,7 @@ import { SearchFieldSkeleton, useFilter, } from "components/Filter/filter" +import { UserFilterMenu, UserMenu } from "components/Filter/UserFilter" import { workspaceFilterQuery } from "utils/filters" const PRESET_FILTERS = [ @@ -68,46 +68,6 @@ export const WorkspacesFilter = ({ ) } -const UserMenu = (menu: UserFilterMenu) => { - return ( - - ) : ( - "All users" - ) - } - > - {(itemProps) => } - - ) -} - -const UserOptionItem = ({ - option, - isSelected, -}: { - option: UserOption - isSelected?: boolean -}) => { - return ( - - } - /> - ) -} - const TemplateMenu = (menu: TemplateFilterMenu) => { return ( , - "value" | "onChange" | "enabled" ->) => { - const me = useMe() - - const addMeAsFirstOption = (options: UserOption[]) => { - options = options.filter((option) => option.value !== me.username) - return [ - { label: me.username, value: me.username, avatarUrl: me.avatar_url }, - ...options, - ] - } - - return useFilterMenu({ - onChange, - enabled, - value, - id: "owner", - getSelectedOption: async () => { - if (value === "me") { - return { - label: me.username, - value: me.username, - avatarUrl: me.avatar_url, - } - } - - const usersRes = await getUsers({ q: value, limit: 1 }) - const firstUser = usersRes.users.at(0) - if (firstUser && firstUser.username === value) { - return { - label: firstUser.username, - value: firstUser.username, - avatarUrl: firstUser.avatar_url, - } - } - return null - }, - getOptions: async (query) => { - const usersRes = await getUsers({ q: query, limit: 25 }) - let options: UserOption[] = usersRes.users.map((user) => ({ - label: user.username, - value: user.username, - avatarUrl: user.avatar_url, - })) - options = addMeAsFirstOption(options) - return options - }, - }) -} - -export type UserFilterMenu = ReturnType - export const useTemplateFilterMenu = ({ value, onChange, diff --git a/site/src/pages/WorkspacesPage/filter/options.ts b/site/src/pages/WorkspacesPage/filter/options.ts index 7e0504dbb7516..2e1b112248cf9 100644 --- a/site/src/pages/WorkspacesPage/filter/options.ts +++ b/site/src/pages/WorkspacesPage/filter/options.ts @@ -1,9 +1,5 @@ import { BaseOption } from "components/Filter/options" -export type UserOption = BaseOption & { - avatarUrl?: string -} - export type StatusOption = BaseOption & { color: string } diff --git a/site/src/xServices/audit/auditXService.ts b/site/src/xServices/audit/auditXService.ts deleted file mode 100644 index d5ecd3a6771b4..0000000000000 --- a/site/src/xServices/audit/auditXService.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { getAuditLogs } from "api/api" -import { getErrorMessage } from "api/errors" -import { AuditLog, AuditLogResponse } from "api/typesGenerated" -import { displayError } from "components/GlobalSnackbar/utils" -import { getPaginationData } from "components/PaginationWidget/utils" -import { - PaginationContext, - PaginationMachineRef, - paginationMachine, -} from "xServices/pagination/paginationXService" -import { assign, createMachine, spawn, send } from "xstate" - -const auditPaginationId = "auditPagination" - -interface AuditContext { - auditLogs?: AuditLog[] - count?: number - filter: string - paginationContext: PaginationContext - paginationRef?: PaginationMachineRef - apiError?: Error | unknown -} - -export const auditMachine = createMachine( - { - id: "auditMachine", - predictableActionArguments: true, - tsTypes: {} as import("./auditXService.typegen").Typegen0, - schema: { - context: {} as AuditContext, - services: {} as { - loadAuditLogsAndCount: { - data: AuditLogResponse - } - }, - events: {} as - | { - type: "UPDATE_PAGE" - page: string - } - | { - type: "FILTER" - filter: string - }, - }, - initial: "startPagination", - states: { - startPagination: { - entry: "assignPaginationRef", - always: "loading", - }, - loading: { - // Right now, XState doesn't a good job with state + context typing so - // this forces the AuditPageView to showing the loading state when the - // loading state is called again by cleaning up the audit logs data - entry: ["clearPreviousAuditLogs", "clearError"], - invoke: { - src: "loadAuditLogsAndCount", - onDone: { - target: "idle", - actions: ["assignAuditLogsAndCount"], - }, - onError: { - target: "idle", - actions: ["displayApiError", "assignError"], - }, - }, - onDone: "idle", - }, - idle: { - on: { - UPDATE_PAGE: { - actions: ["updateURL"], - target: "loading", - }, - FILTER: { - actions: ["assignFilter", "sendResetPage"], - }, - }, - }, - }, - }, - { - actions: { - clearPreviousAuditLogs: assign({ - auditLogs: (_) => undefined, - }), - assignAuditLogsAndCount: assign({ - auditLogs: (_, event) => event.data.audit_logs, - count: (_, event) => event.data.count, - }), - assignPaginationRef: assign({ - paginationRef: (context) => - spawn( - paginationMachine.withContext(context.paginationContext), - auditPaginationId, - ), - }), - assignFilter: assign({ - filter: (_, { filter }) => filter, - }), - assignError: assign({ - apiError: (_, event) => event.data, - }), - clearError: assign({ - apiError: (_) => undefined, - }), - displayApiError: (_, event) => { - const message = getErrorMessage( - event.data, - "Error on loading audit logs.", - ) - displayError(message) - }, - sendResetPage: send({ type: "RESET_PAGE" }, { to: auditPaginationId }), - }, - services: { - loadAuditLogsAndCount: async (context) => { - if (context.paginationRef) { - const { offset, limit } = getPaginationData(context.paginationRef) - return getAuditLogs({ - offset, - limit, - q: context.filter, - }) - } else { - throw new Error("Cannot get audit logs without pagination data") - } - }, - }, - }, -)