feat: add organizations filter to audit table (#13978)

* Ignore organization ID in member and role audit logs

Since the organization will never change in any resources, 
and the org is already on the top-level of the response. 

* Add organization details and filter to audit table

These only display if the multi-org experiment is enabled.

This also includes a modification to customize the width 
of the filters since with four things get a bit squishy.

* Add more audit mocks

To test different org names and no org.
This commit is contained in:
Asher
2024-07-24 14:28:23 -08:00
committed by GitHub
parent 4f01372179
commit e8b3db8c7a
10 changed files with 235 additions and 19 deletions

View File

@ -13,8 +13,8 @@ We track the following resources:
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> | | APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> | | AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> | | Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>true</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> | | AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>true</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> | | CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> | | GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> | | HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> | | License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |

View File

@ -53,7 +53,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
&database.AuditableOrganizationMember{}: { &database.AuditableOrganizationMember{}: {
"username": ActionTrack, "username": ActionTrack,
"user_id": ActionTrack, "user_id": ActionTrack,
"organization_id": ActionTrack, "organization_id": ActionIgnore, // Never changes.
"created_at": ActionTrack, "created_at": ActionTrack,
"updated_at": ActionTrack, "updated_at": ActionTrack,
"roles": ActionTrack, "roles": ActionTrack,
@ -64,7 +64,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"site_permissions": ActionTrack, "site_permissions": ActionTrack,
"org_permissions": ActionTrack, "org_permissions": ActionTrack,
"user_permissions": ActionTrack, "user_permissions": ActionTrack,
"organization_id": ActionTrack, "organization_id": ActionIgnore, // Never changes.
"id": ActionIgnore, "id": ActionIgnore,
"created_at": ActionIgnore, "created_at": ActionIgnore,

View File

@ -32,6 +32,7 @@ export type SelectFilterProps = {
onSelect: (option: SelectFilterOption | undefined) => void; onSelect: (option: SelectFilterOption | undefined) => void;
// SelectFilterSearch element // SelectFilterSearch element
selectFilterSearch?: ReactNode; selectFilterSearch?: ReactNode;
width?: number;
}; };
export const SelectFilter: FC<SelectFilterProps> = ({ export const SelectFilter: FC<SelectFilterProps> = ({
@ -42,6 +43,7 @@ export const SelectFilter: FC<SelectFilterProps> = ({
placeholder, placeholder,
emptyText, emptyText,
selectFilterSearch, selectFilterSearch,
width = BASE_WIDTH,
}) => { }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -50,7 +52,7 @@ export const SelectFilter: FC<SelectFilterProps> = ({
<SelectMenuTrigger> <SelectMenuTrigger>
<SelectMenuButton <SelectMenuButton
startIcon={selectedOption?.startIcon} startIcon={selectedOption?.startIcon}
css={{ width: BASE_WIDTH }} css={{ width }}
aria-label={label} aria-label={label}
> >
{selectedOption?.label ?? placeholder} {selectedOption?.label ?? placeholder}
@ -64,7 +66,7 @@ export const SelectFilter: FC<SelectFilterProps> = ({
// wide as possible. // wide as possible.
width: selectFilterSearch ? "100%" : undefined, width: selectFilterSearch ? "100%" : undefined,
maxWidth: POPOVER_WIDTH, maxWidth: POPOVER_WIDTH,
minWidth: BASE_WIDTH, minWidth: width,
}, },
}} }}
> >

View File

@ -97,9 +97,10 @@ export type UserFilterMenu = ReturnType<typeof useUserFilterMenu>;
interface UserMenuProps { interface UserMenuProps {
menu: UserFilterMenu; menu: UserFilterMenu;
width?: number;
} }
export const UserMenu: FC<UserMenuProps> = ({ menu }) => { export const UserMenu: FC<UserMenuProps> = ({ menu, width }) => {
return ( return (
<SelectFilter <SelectFilter
label="Select user" label="Select user"
@ -116,6 +117,7 @@ export const UserMenu: FC<UserMenuProps> = ({ menu }) => {
onChange={menu.setQuery} onChange={menu.setQuery}
/> />
} }
width={width}
/> />
); );
}; };

View File

@ -1,5 +1,6 @@
import capitalize from "lodash/capitalize"; import capitalize from "lodash/capitalize";
import type { FC } from "react"; import type { FC } from "react";
import { API } from "api/api";
import { AuditActions, ResourceTypes } from "api/typesGenerated"; import { AuditActions, ResourceTypes } from "api/typesGenerated";
import { import {
Filter, Filter,
@ -13,9 +14,11 @@ import {
} from "components/Filter/menu"; } from "components/Filter/menu";
import { import {
SelectFilter, SelectFilter,
SelectFilterSearch,
type SelectFilterOption, type SelectFilterOption,
} from "components/Filter/SelectFilter"; } from "components/Filter/SelectFilter";
import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter";
import { UserAvatar } from "components/UserAvatar/UserAvatar";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
const PRESET_FILTERS = [ const PRESET_FILTERS = [
@ -42,10 +45,14 @@ interface AuditFilterProps {
user: UserFilterMenu; user: UserFilterMenu;
action: ActionFilterMenu; action: ActionFilterMenu;
resourceType: ResourceTypeFilterMenu; resourceType: ResourceTypeFilterMenu;
// The organization menu is only provided in a multi-org setup.
organization?: OrganizationsFilterMenu;
}; };
} }
export const AuditFilter: FC<AuditFilterProps> = ({ filter, error, menus }) => { export const AuditFilter: FC<AuditFilterProps> = ({ filter, error, menus }) => {
// Use a smaller width if including the organization filter.
const width = menus.organization && 175;
return ( return (
<Filter <Filter
learnMoreLink={docs("/admin/audit-logs#filtering-logs")} learnMoreLink={docs("/admin/audit-logs#filtering-logs")}
@ -55,9 +62,12 @@ export const AuditFilter: FC<AuditFilterProps> = ({ filter, error, menus }) => {
error={error} error={error}
options={ options={
<> <>
<ResourceTypeMenu {...menus.resourceType} /> <ResourceTypeMenu width={width} menu={menus.resourceType} />
<ActionMenu {...menus.action} /> <ActionMenu width={width} menu={menus.action} />
<UserMenu menu={menus.user} /> <UserMenu width={width} menu={menus.user} />
{menus.organization && (
<OrganizationsMenu width={width} menu={menus.organization} />
)}
</> </>
} }
skeleton={ skeleton={
@ -92,7 +102,12 @@ export const useActionFilterMenu = ({
export type ActionFilterMenu = ReturnType<typeof useActionFilterMenu>; export type ActionFilterMenu = ReturnType<typeof useActionFilterMenu>;
const ActionMenu = (menu: ActionFilterMenu) => { interface ActionMenuProps {
menu: ActionFilterMenu;
width?: number;
}
const ActionMenu: FC<ActionMenuProps> = ({ menu, width }) => {
return ( return (
<SelectFilter <SelectFilter
label="Select an action" label="Select an action"
@ -100,6 +115,7 @@ const ActionMenu = (menu: ActionFilterMenu) => {
options={menu.searchOptions} options={menu.searchOptions}
onSelect={menu.selectOption} onSelect={menu.selectOption}
selectedOption={menu.selectedOption ?? undefined} selectedOption={menu.selectedOption ?? undefined}
width={width}
/> />
); );
}; };
@ -146,7 +162,12 @@ export type ResourceTypeFilterMenu = ReturnType<
typeof useResourceTypeFilterMenu typeof useResourceTypeFilterMenu
>; >;
const ResourceTypeMenu = (menu: ResourceTypeFilterMenu) => { interface ResourceTypeMenuProps {
menu: ResourceTypeFilterMenu;
width?: number;
}
const ResourceTypeMenu: FC<ResourceTypeMenuProps> = ({ menu, width }) => {
return ( return (
<SelectFilter <SelectFilter
label="Select a resource type" label="Select a resource type"
@ -154,6 +175,88 @@ const ResourceTypeMenu = (menu: ResourceTypeFilterMenu) => {
options={menu.searchOptions} options={menu.searchOptions}
onSelect={menu.selectOption} onSelect={menu.selectOption}
selectedOption={menu.selectedOption ?? undefined} selectedOption={menu.selectedOption ?? undefined}
width={width}
/>
);
};
export const useOrganizationsFilterMenu = ({
value,
onChange,
}: Pick<UseFilterMenuOptions<SelectFilterOption>, "value" | "onChange">) => {
return useFilterMenu({
onChange,
value,
id: "organizations",
getSelectedOption: async () => {
if (value) {
const organizations = await API.getOrganizations();
const organization = organizations.find((o) => o.name === value);
if (organization) {
return {
label: organization.display_name || organization.name,
value: organization.name,
startIcon: (
<UserAvatar
key={organization.id}
size="xs"
username={organization.display_name || organization.name}
avatarURL={organization.icon}
/>
),
};
}
}
return null;
},
getOptions: async () => {
const organizationsRes = await API.getOrganizations();
return organizationsRes.map<SelectFilterOption>((organization) => ({
label: organization.display_name || organization.name,
value: organization.name,
startIcon: (
<UserAvatar
key={organization.id}
size="xs"
username={organization.display_name || organization.name}
avatarURL={organization.icon}
/>
),
}));
},
});
};
export type OrganizationsFilterMenu = ReturnType<
typeof useOrganizationsFilterMenu
>;
interface OrganizationsMenuProps {
menu: OrganizationsFilterMenu;
width?: number;
}
export const OrganizationsMenu: FC<OrganizationsMenuProps> = ({
menu,
width,
}) => {
return (
<SelectFilter
label="Select an organization"
placeholder="All organizations"
emptyText="No organizations found"
options={menu.searchOptions}
onSelect={menu.selectOption}
selectedOption={menu.selectedOption ?? undefined}
selectFilterSearch={
<SelectFilterSearch
inputProps={{ "aria-label": "Search organization" }}
placeholder="Search organization..."
value={menu.query}
onChange={menu.setQuery}
/>
}
width={width}
/> />
); );
}; };

View File

@ -1,7 +1,9 @@
import type { CSSObject, Interpolation, Theme } from "@emotion/react"; import type { CSSObject, Interpolation, Theme } from "@emotion/react";
import Collapse from "@mui/material/Collapse"; import Collapse from "@mui/material/Collapse";
import Link from "@mui/material/Link";
import TableCell from "@mui/material/TableCell"; import TableCell from "@mui/material/TableCell";
import { type FC, useState } from "react"; import { type FC, useState } from "react";
import { Link as RouterLink } from "react-router-dom";
import userAgentParser from "ua-parser-js"; import userAgentParser from "ua-parser-js";
import type { AuditLog } from "api/typesGenerated"; import type { AuditLog } from "api/typesGenerated";
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
@ -33,11 +35,13 @@ export interface AuditLogRowProps {
auditLog: AuditLog; auditLog: AuditLog;
// Useful for Storybook // Useful for Storybook
defaultIsDiffOpen?: boolean; defaultIsDiffOpen?: boolean;
showOrgDetails: boolean;
} }
export const AuditLogRow: FC<AuditLogRowProps> = ({ export const AuditLogRow: FC<AuditLogRowProps> = ({
auditLog, auditLog,
defaultIsDiffOpen = false, defaultIsDiffOpen = false,
showOrgDetails,
}) => { }) => {
const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen); const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen);
const diffs = Object.entries(auditLog.diff); const diffs = Object.entries(auditLog.diff);
@ -132,6 +136,20 @@ export const AuditLogRow: FC<AuditLogRowProps> = ({
</strong> </strong>
</span> </span>
)} )}
{showOrgDetails && auditLog.organization && (
<span css={styles.auditLogInfo}>
<>Org: </>
<Link
component={RouterLink}
to={`/organizations/${auditLog.organization.name}`}
>
<strong>
{auditLog.organization.display_name ||
auditLog.organization.name}
</strong>
</Link>
</span>
)}
</Stack> </Stack>
<Pill <Pill

View File

@ -6,13 +6,19 @@ import { useFilter } from "components/Filter/filter";
import { useUserFilterMenu } from "components/Filter/UserFilter"; import { useUserFilterMenu } from "components/Filter/UserFilter";
import { isNonInitialPage } from "components/PaginationWidget/utils"; import { isNonInitialPage } from "components/PaginationWidget/utils";
import { usePaginatedQuery } from "hooks/usePaginatedQuery"; import { usePaginatedQuery } from "hooks/usePaginatedQuery";
import { useDashboard } from "modules/dashboard/useDashboard";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { pageTitle } from "utils/page"; import { pageTitle } from "utils/page";
import { useActionFilterMenu, useResourceTypeFilterMenu } from "./AuditFilter"; import {
useActionFilterMenu,
useOrganizationsFilterMenu,
useResourceTypeFilterMenu,
} from "./AuditFilter";
import { AuditPageView } from "./AuditPageView"; import { AuditPageView } from "./AuditPageView";
const AuditPage: FC = () => { const AuditPage: FC = () => {
const { audit_log: isAuditLogVisible } = useFeatureVisibility(); const { audit_log: isAuditLogVisible } = useFeatureVisibility();
const { experiments } = useDashboard();
/** /**
* There is an implicit link between auditsQuery and filter via the * There is an implicit link between auditsQuery and filter via the
@ -55,6 +61,15 @@ const AuditPage: FC = () => {
}), }),
}); });
const organizationsMenu = useOrganizationsFilterMenu({
value: filter.values.organization,
onChange: (option) =>
filter.update({
...filter.values,
organization: option?.value,
}),
});
return ( return (
<> <>
<Helmet> <Helmet>
@ -67,6 +82,7 @@ const AuditPage: FC = () => {
isAuditLogVisible={isAuditLogVisible} isAuditLogVisible={isAuditLogVisible}
auditsQuery={auditsQuery} auditsQuery={auditsQuery}
error={auditsQuery.error} error={auditsQuery.error}
showOrgDetails={experiments.includes("multi-organization")}
filterProps={{ filterProps={{
filter, filter,
error: auditsQuery.error, error: auditsQuery.error,
@ -74,6 +90,9 @@ const AuditPage: FC = () => {
user: userMenu, user: userMenu,
action: actionMenu, action: actionMenu,
resourceType: resourceTypeMenu, resourceType: resourceTypeMenu,
organization: experiments.includes("multi-organization")
? organizationsMenu
: undefined,
}, },
}} }}
/> />

View File

@ -10,7 +10,12 @@ import {
} from "components/PaginationWidget/PaginationContainer.mocks"; } from "components/PaginationWidget/PaginationContainer.mocks";
import type { UsePaginatedQueryResult } from "hooks/usePaginatedQuery"; import type { UsePaginatedQueryResult } from "hooks/usePaginatedQuery";
import { chromaticWithTablet } from "testHelpers/chromatic"; import { chromaticWithTablet } from "testHelpers/chromatic";
import { MockAuditLog, MockAuditLog2, MockUser } from "testHelpers/entities"; import {
MockAuditLog,
MockAuditLog2,
MockAuditLog3,
MockUser,
} from "testHelpers/entities";
import { AuditPageView } from "./AuditPageView"; import { AuditPageView } from "./AuditPageView";
type FilterProps = ComponentProps<typeof AuditPageView>["filterProps"]; type FilterProps = ComponentProps<typeof AuditPageView>["filterProps"];
@ -21,6 +26,7 @@ const defaultFilterProps = getDefaultFilterProps<FilterProps>({
username: MockUser.username, username: MockUser.username,
action: undefined, action: undefined,
resource_type: undefined, resource_type: undefined,
organization: undefined,
}, },
menus: { menus: {
user: MockMenu, user: MockMenu,
@ -33,9 +39,10 @@ const meta: Meta<typeof AuditPageView> = {
title: "pages/AuditPage", title: "pages/AuditPage",
component: AuditPageView, component: AuditPageView,
args: { args: {
auditLogs: [MockAuditLog, MockAuditLog2], auditLogs: [MockAuditLog, MockAuditLog2, MockAuditLog3],
isAuditLogVisible: true, isAuditLogVisible: true,
filterProps: defaultFilterProps, filterProps: defaultFilterProps,
showOrgDetails: false,
}, },
}; };
@ -85,3 +92,18 @@ export const NotVisible: Story = {
auditsQuery: mockInitialRenderResult, auditsQuery: mockInitialRenderResult,
}, },
}; };
export const MultiOrg: Story = {
parameters: { chromatic: chromaticWithTablet },
args: {
showOrgDetails: true,
auditsQuery: mockSuccessResult,
filterProps: {
...defaultFilterProps,
menus: {
...defaultFilterProps.menus,
organization: MockMenu,
},
},
},
};

View File

@ -38,6 +38,7 @@ export interface AuditPageViewProps {
error?: unknown; error?: unknown;
filterProps: ComponentProps<typeof AuditFilter>; filterProps: ComponentProps<typeof AuditFilter>;
auditsQuery: PaginationResult; auditsQuery: PaginationResult;
showOrgDetails: boolean;
} }
export const AuditPageView: FC<AuditPageViewProps> = ({ export const AuditPageView: FC<AuditPageViewProps> = ({
@ -47,6 +48,7 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
error, error,
filterProps, filterProps,
auditsQuery: paginationResult, auditsQuery: paginationResult,
showOrgDetails,
}) => { }) => {
const isLoading = const isLoading =
(auditLogs === undefined || paginationResult.totalRecords === undefined) && (auditLogs === undefined || paginationResult.totalRecords === undefined) &&
@ -117,7 +119,11 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
items={auditLogs} items={auditLogs}
getDate={(log) => new Date(log.time)} getDate={(log) => new Date(log.time)}
row={(log) => ( row={(log) => (
<AuditLogRow key={log.id} auditLog={log} /> <AuditLogRow
key={log.id}
auditLog={log}
showOrgDetails={showOrgDetails}
/>
)} )}
/> />
)} )}

View File

@ -2211,6 +2211,9 @@ export const MockEntitlementsWithUserLimit: TypesGen.Entitlements = {
export const MockExperiments: TypesGen.Experiment[] = []; export const MockExperiments: TypesGen.Experiment[] = [];
/**
* An audit log for MockOrganization.
*/
export const MockAuditLog: TypesGen.AuditLog = { export const MockAuditLog: TypesGen.AuditLog = {
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
request_id: "53bded77-7b9d-4e82-8771-991a34d759f9", request_id: "53bded77-7b9d-4e82-8771-991a34d759f9",
@ -2218,9 +2221,9 @@ export const MockAuditLog: TypesGen.AuditLog = {
organization_id: MockOrganization.id, organization_id: MockOrganization.id,
organization: { organization: {
id: MockOrganization.id, id: MockOrganization.id,
name: "mock name", name: MockOrganization.name,
display_name: "mock display name", display_name: MockOrganization.display_name,
icon: "/emojis/1f48f-1f3ff.png", icon: MockOrganization.icon,
}, },
ip: "127.0.0.1", ip: "127.0.0.1",
user_agent: user_agent:
@ -2245,12 +2248,22 @@ export const MockAuditLog: TypesGen.AuditLog = {
is_deleted: false, is_deleted: false,
}; };
/**
* An audit log for MockOrganization2.
*/
export const MockAuditLog2: TypesGen.AuditLog = { export const MockAuditLog2: TypesGen.AuditLog = {
...MockAuditLog, ...MockAuditLog,
id: "53bded77-7b9d-4e82-8771-991a34d759f9", id: "53bded77-7b9d-4e82-8771-991a34d759f9",
action: "write", action: "write",
time: "2022-05-20T16:45:57.122Z", time: "2022-05-20T16:45:57.122Z",
description: "{user} updated workspace {target}", description: "{user} updated workspace {target}",
organization_id: MockOrganization2.id,
organization: {
id: MockOrganization2.id,
name: MockOrganization2.name,
display_name: MockOrganization2.display_name,
icon: MockOrganization2.icon,
},
diff: { diff: {
workspace_name: { workspace_name: {
old: "old-workspace-name", old: "old-workspace-name",
@ -2275,6 +2288,37 @@ export const MockAuditLog2: TypesGen.AuditLog = {
}, },
}; };
/**
* An audit log without an organization.
*/
export const MockAuditLog3: TypesGen.AuditLog = {
id: "8efa9208-656a-422d-842d-b9dec0cf1bf3",
request_id: "57ee9510-8330-480d-9ffa-4024e5805465",
time: "2024-06-11T01:32:11.123Z",
organization_id: "00000000-0000-0000-000000000000",
ip: "127.0.0.1",
user_agent:
'"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"',
resource_type: "template",
resource_id: "a624458c-1562-4689-a671-42c0b7d2d0c5",
resource_target: "docker",
resource_icon: "",
action: "write",
diff: {
display_name: {
old: "old display",
new: "new display",
secret: false,
},
},
status_code: 200,
additional_fields: {},
description: "{user} updated template {target}",
user: MockUser,
resource_link: "/templates/docker",
is_deleted: false,
};
export const MockWorkspaceCreateAuditLogForDifferentOwner = { export const MockWorkspaceCreateAuditLogForDifferentOwner = {
...MockAuditLog, ...MockAuditLog,
additional_fields: { additional_fields: {