Skip to content

feat(site): add new filter to audit logs #7878

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add basic filter for audit
  • Loading branch information
BrunoQuaresma committed Jun 6, 2023
commit bf39e76c677b25baa915738a2d3d1f6c842c59e6
100 changes: 100 additions & 0 deletions site/src/components/Filter/UserFilter.tsx
Original file line number Diff line number Diff line change
@@ -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<UserOption>,
"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 })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We call getUsers here, and also on line 48. Curious why we can't just make one call to grab all users and then filter on the user we want for getSelectedOption.

Copy link
Collaborator Author

@BrunoQuaresma BrunoQuaresma Jun 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, let's say we have a user called Wesley, Wesley is probably one of the last users to be queried in the database, so when we display the autocomplete options for the users, maybe Wesley is not shown because we only show the first 25 results. So making a separate query to get the selected value, in this case Wesley, is safer and we can be sure we are going to find it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, thanks for explaining!

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<typeof useUserFilterMenu>

export const UserMenu = (menu: UserFilterMenu) => {
return (
<FilterSearchMenu
id="users-menu"
menu={menu}
label={
menu.selectedOption ? (
<UserOptionItem option={menu.selectedOption} />
) : (
"All users"
)
}
>
{(itemProps) => <UserOptionItem {...itemProps} />}
</FilterSearchMenu>
)
}

const UserOptionItem = ({
option,
isSelected,
}: {
option: UserOption
isSelected?: boolean
}) => {
return (
<OptionItem
option={option}
isSelected={isSelected}
left={
<UserAvatar
username={option.label}
avatarURL={option.avatarUrl}
sx={{ width: 16, height: 16, fontSize: 8 }}
/>
}
/>
)
}
34 changes: 34 additions & 0 deletions site/src/pages/AuditPage/AuditFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { UserFilterMenu, UserMenu } from "components/Filter/UserFilter"
import {
Filter,
MenuSkeleton,
SearchFieldSkeleton,
useFilter,
} from "components/Filter/filter"

export const AuditFilter = ({
filter,
error,
menus,
}: {
filter: ReturnType<typeof useFilter>
error?: unknown
menus: {
user: UserFilterMenu
}
}) => {
return (
<Filter
isLoading={menus.user.isInitializing}
filter={filter}
error={error}
options={<UserMenu {...menus.user} />}
skeleton={
<>
<SearchFieldSkeleton />
<MenuSkeleton />
</>
}
/>
)
}
81 changes: 52 additions & 29 deletions site/src/pages/AuditPage/AuditPage.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,74 @@
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"

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 { 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 (
<>
<Helmet>
<title>{pageTitle("Audit")}</title>
</Helmet>
<AuditPageView
filter={filter}
auditLogs={auditLogs}
count={count}
onFilter={(filter) => {
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,
},
}
: {
filter: filter.query,
onFilter: filter.update,
}
}
/>
</>
)
Expand Down
54 changes: 38 additions & 16 deletions site/src/pages/AuditPage/AuditPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<typeof SearchBarWithFilter>
| ComponentProps<typeof AuditFilter>
}

export const AuditPageView: FC<AuditPageViewProps> = ({
auditLogs,
count,
filter,
onFilter,
paginationRef,
page,
limit,
onPageChange,
isNonInitialPage,
isAuditLogVisible,
error,
filterProps,
}) => {
const { t } = useTranslation("auditLog")

Expand All @@ -86,12 +91,22 @@ export const AuditPageView: FC<AuditPageViewProps> = ({

<ChooseOne>
<Cond condition={isAuditLogVisible}>
<SearchBarWithFilter
docs="https://coder.com/docs/coder-oss/latest/admin/audit-logs#filtering-logs"
filter={filter}
onFilter={onFilter}
presetFilters={presetFilters}
error={error}
{"onFilter" in filterProps ? (
<SearchBarWithFilter
{...filterProps}
docs="https://coder.com/docs/coder-oss/latest/admin/audit-logs#filtering-logs"
presetFilters={presetFilters}
error={error}
/>
) : (
<AuditFilter {...filterProps} />
)}

<PaginationStatus
isLoading={Boolean(isLoading)}
showing={auditLogs?.length}
total={count}
label="audit logs"
/>

<TableContainer>
Expand Down Expand Up @@ -143,7 +158,14 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
</Table>
</TableContainer>

<PaginationWidget numRecords={count} paginationRef={paginationRef} />
{count !== undefined && (
<PaginationWidgetBase
count={count}
limit={limit}
onChange={onPageChange}
page={page}
/>
)}
</Cond>

<Cond>
Expand Down
9 changes: 3 additions & 6 deletions site/src/pages/WorkspacesPage/WorkspacesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ import { pageTitle } from "utils/page"
import { useWorkspacesData, useWorkspaceUpdate } from "./data"
import { WorkspacesPageView } from "./WorkspacesPageView"
import { useMe, 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 me = useMe()
Expand Down Expand Up @@ -68,6 +65,7 @@ const WorkspacesPage: FC = () => {
count={data?.count}
page={pagination.page}
limit={pagination.limit}
onPageChange={pagination.goToPage}
filterProps={{
filter,
menus: {
Expand All @@ -76,7 +74,6 @@ const WorkspacesPage: FC = () => {
status: statusMenu,
},
}}
onPageChange={pagination.goToPage}
onUpdateWorkspace={(workspace) => {
updateWorkspace.mutate(workspace)
}}
Expand Down
2 changes: 1 addition & 1 deletion site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ export interface WorkspacesPageViewProps {
workspaces?: Workspace[]
count?: number
useNewFilter?: boolean
filterProps: ComponentProps<typeof WorkspacesFilter>
page: number
limit: number
filterProps: ComponentProps<typeof WorkspacesFilter>
onPageChange: (page: number) => void
onUpdateWorkspace: (workspace: Workspace) => void
}
Expand Down
Loading