mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
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:
@ -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> |
|
||||
| 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> |
|
||||
| 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> |
|
||||
| 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> |
|
||||
| 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>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> |
|
||||
| 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> |
|
||||
|
@ -53,7 +53,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
&database.AuditableOrganizationMember{}: {
|
||||
"username": ActionTrack,
|
||||
"user_id": ActionTrack,
|
||||
"organization_id": ActionTrack,
|
||||
"organization_id": ActionIgnore, // Never changes.
|
||||
"created_at": ActionTrack,
|
||||
"updated_at": ActionTrack,
|
||||
"roles": ActionTrack,
|
||||
@ -64,7 +64,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
"site_permissions": ActionTrack,
|
||||
"org_permissions": ActionTrack,
|
||||
"user_permissions": ActionTrack,
|
||||
"organization_id": ActionTrack,
|
||||
"organization_id": ActionIgnore, // Never changes.
|
||||
|
||||
"id": ActionIgnore,
|
||||
"created_at": ActionIgnore,
|
||||
|
@ -32,6 +32,7 @@ export type SelectFilterProps = {
|
||||
onSelect: (option: SelectFilterOption | undefined) => void;
|
||||
// SelectFilterSearch element
|
||||
selectFilterSearch?: ReactNode;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export const SelectFilter: FC<SelectFilterProps> = ({
|
||||
@ -42,6 +43,7 @@ export const SelectFilter: FC<SelectFilterProps> = ({
|
||||
placeholder,
|
||||
emptyText,
|
||||
selectFilterSearch,
|
||||
width = BASE_WIDTH,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@ -50,7 +52,7 @@ export const SelectFilter: FC<SelectFilterProps> = ({
|
||||
<SelectMenuTrigger>
|
||||
<SelectMenuButton
|
||||
startIcon={selectedOption?.startIcon}
|
||||
css={{ width: BASE_WIDTH }}
|
||||
css={{ width }}
|
||||
aria-label={label}
|
||||
>
|
||||
{selectedOption?.label ?? placeholder}
|
||||
@ -64,7 +66,7 @@ export const SelectFilter: FC<SelectFilterProps> = ({
|
||||
// wide as possible.
|
||||
width: selectFilterSearch ? "100%" : undefined,
|
||||
maxWidth: POPOVER_WIDTH,
|
||||
minWidth: BASE_WIDTH,
|
||||
minWidth: width,
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
@ -97,9 +97,10 @@ export type UserFilterMenu = ReturnType<typeof useUserFilterMenu>;
|
||||
|
||||
interface UserMenuProps {
|
||||
menu: UserFilterMenu;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const UserMenu: FC<UserMenuProps> = ({ menu }) => {
|
||||
export const UserMenu: FC<UserMenuProps> = ({ menu, width }) => {
|
||||
return (
|
||||
<SelectFilter
|
||||
label="Select user"
|
||||
@ -116,6 +117,7 @@ export const UserMenu: FC<UserMenuProps> = ({ menu }) => {
|
||||
onChange={menu.setQuery}
|
||||
/>
|
||||
}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import capitalize from "lodash/capitalize";
|
||||
import type { FC } from "react";
|
||||
import { API } from "api/api";
|
||||
import { AuditActions, ResourceTypes } from "api/typesGenerated";
|
||||
import {
|
||||
Filter,
|
||||
@ -13,9 +14,11 @@ import {
|
||||
} from "components/Filter/menu";
|
||||
import {
|
||||
SelectFilter,
|
||||
SelectFilterSearch,
|
||||
type SelectFilterOption,
|
||||
} from "components/Filter/SelectFilter";
|
||||
import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter";
|
||||
import { UserAvatar } from "components/UserAvatar/UserAvatar";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
const PRESET_FILTERS = [
|
||||
@ -42,10 +45,14 @@ interface AuditFilterProps {
|
||||
user: UserFilterMenu;
|
||||
action: ActionFilterMenu;
|
||||
resourceType: ResourceTypeFilterMenu;
|
||||
// The organization menu is only provided in a multi-org setup.
|
||||
organization?: OrganizationsFilterMenu;
|
||||
};
|
||||
}
|
||||
|
||||
export const AuditFilter: FC<AuditFilterProps> = ({ filter, error, menus }) => {
|
||||
// Use a smaller width if including the organization filter.
|
||||
const width = menus.organization && 175;
|
||||
return (
|
||||
<Filter
|
||||
learnMoreLink={docs("/admin/audit-logs#filtering-logs")}
|
||||
@ -55,9 +62,12 @@ export const AuditFilter: FC<AuditFilterProps> = ({ filter, error, menus }) => {
|
||||
error={error}
|
||||
options={
|
||||
<>
|
||||
<ResourceTypeMenu {...menus.resourceType} />
|
||||
<ActionMenu {...menus.action} />
|
||||
<UserMenu menu={menus.user} />
|
||||
<ResourceTypeMenu width={width} menu={menus.resourceType} />
|
||||
<ActionMenu width={width} menu={menus.action} />
|
||||
<UserMenu width={width} menu={menus.user} />
|
||||
{menus.organization && (
|
||||
<OrganizationsMenu width={width} menu={menus.organization} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
skeleton={
|
||||
@ -92,7 +102,12 @@ export const useActionFilterMenu = ({
|
||||
|
||||
export type ActionFilterMenu = ReturnType<typeof useActionFilterMenu>;
|
||||
|
||||
const ActionMenu = (menu: ActionFilterMenu) => {
|
||||
interface ActionMenuProps {
|
||||
menu: ActionFilterMenu;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const ActionMenu: FC<ActionMenuProps> = ({ menu, width }) => {
|
||||
return (
|
||||
<SelectFilter
|
||||
label="Select an action"
|
||||
@ -100,6 +115,7 @@ const ActionMenu = (menu: ActionFilterMenu) => {
|
||||
options={menu.searchOptions}
|
||||
onSelect={menu.selectOption}
|
||||
selectedOption={menu.selectedOption ?? undefined}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -146,7 +162,12 @@ export type ResourceTypeFilterMenu = ReturnType<
|
||||
typeof useResourceTypeFilterMenu
|
||||
>;
|
||||
|
||||
const ResourceTypeMenu = (menu: ResourceTypeFilterMenu) => {
|
||||
interface ResourceTypeMenuProps {
|
||||
menu: ResourceTypeFilterMenu;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const ResourceTypeMenu: FC<ResourceTypeMenuProps> = ({ menu, width }) => {
|
||||
return (
|
||||
<SelectFilter
|
||||
label="Select a resource type"
|
||||
@ -154,6 +175,88 @@ const ResourceTypeMenu = (menu: ResourceTypeFilterMenu) => {
|
||||
options={menu.searchOptions}
|
||||
onSelect={menu.selectOption}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,9 @@
|
||||
import type { CSSObject, Interpolation, Theme } from "@emotion/react";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import Link from "@mui/material/Link";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import { type FC, useState } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import userAgentParser from "ua-parser-js";
|
||||
import type { AuditLog } from "api/typesGenerated";
|
||||
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
|
||||
@ -33,11 +35,13 @@ export interface AuditLogRowProps {
|
||||
auditLog: AuditLog;
|
||||
// Useful for Storybook
|
||||
defaultIsDiffOpen?: boolean;
|
||||
showOrgDetails: boolean;
|
||||
}
|
||||
|
||||
export const AuditLogRow: FC<AuditLogRowProps> = ({
|
||||
auditLog,
|
||||
defaultIsDiffOpen = false,
|
||||
showOrgDetails,
|
||||
}) => {
|
||||
const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen);
|
||||
const diffs = Object.entries(auditLog.diff);
|
||||
@ -132,6 +136,20 @@ export const AuditLogRow: FC<AuditLogRowProps> = ({
|
||||
</strong>
|
||||
</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>
|
||||
|
||||
<Pill
|
||||
|
@ -6,13 +6,19 @@ import { useFilter } from "components/Filter/filter";
|
||||
import { useUserFilterMenu } from "components/Filter/UserFilter";
|
||||
import { isNonInitialPage } from "components/PaginationWidget/utils";
|
||||
import { usePaginatedQuery } from "hooks/usePaginatedQuery";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { useActionFilterMenu, useResourceTypeFilterMenu } from "./AuditFilter";
|
||||
import {
|
||||
useActionFilterMenu,
|
||||
useOrganizationsFilterMenu,
|
||||
useResourceTypeFilterMenu,
|
||||
} from "./AuditFilter";
|
||||
import { AuditPageView } from "./AuditPageView";
|
||||
|
||||
const AuditPage: FC = () => {
|
||||
const { audit_log: isAuditLogVisible } = useFeatureVisibility();
|
||||
const { experiments } = useDashboard();
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<>
|
||||
<Helmet>
|
||||
@ -67,6 +82,7 @@ const AuditPage: FC = () => {
|
||||
isAuditLogVisible={isAuditLogVisible}
|
||||
auditsQuery={auditsQuery}
|
||||
error={auditsQuery.error}
|
||||
showOrgDetails={experiments.includes("multi-organization")}
|
||||
filterProps={{
|
||||
filter,
|
||||
error: auditsQuery.error,
|
||||
@ -74,6 +90,9 @@ const AuditPage: FC = () => {
|
||||
user: userMenu,
|
||||
action: actionMenu,
|
||||
resourceType: resourceTypeMenu,
|
||||
organization: experiments.includes("multi-organization")
|
||||
? organizationsMenu
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -10,7 +10,12 @@ import {
|
||||
} from "components/PaginationWidget/PaginationContainer.mocks";
|
||||
import type { UsePaginatedQueryResult } from "hooks/usePaginatedQuery";
|
||||
import { chromaticWithTablet } from "testHelpers/chromatic";
|
||||
import { MockAuditLog, MockAuditLog2, MockUser } from "testHelpers/entities";
|
||||
import {
|
||||
MockAuditLog,
|
||||
MockAuditLog2,
|
||||
MockAuditLog3,
|
||||
MockUser,
|
||||
} from "testHelpers/entities";
|
||||
import { AuditPageView } from "./AuditPageView";
|
||||
|
||||
type FilterProps = ComponentProps<typeof AuditPageView>["filterProps"];
|
||||
@ -21,6 +26,7 @@ const defaultFilterProps = getDefaultFilterProps<FilterProps>({
|
||||
username: MockUser.username,
|
||||
action: undefined,
|
||||
resource_type: undefined,
|
||||
organization: undefined,
|
||||
},
|
||||
menus: {
|
||||
user: MockMenu,
|
||||
@ -33,9 +39,10 @@ const meta: Meta<typeof AuditPageView> = {
|
||||
title: "pages/AuditPage",
|
||||
component: AuditPageView,
|
||||
args: {
|
||||
auditLogs: [MockAuditLog, MockAuditLog2],
|
||||
auditLogs: [MockAuditLog, MockAuditLog2, MockAuditLog3],
|
||||
isAuditLogVisible: true,
|
||||
filterProps: defaultFilterProps,
|
||||
showOrgDetails: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -85,3 +92,18 @@ export const NotVisible: Story = {
|
||||
auditsQuery: mockInitialRenderResult,
|
||||
},
|
||||
};
|
||||
|
||||
export const MultiOrg: Story = {
|
||||
parameters: { chromatic: chromaticWithTablet },
|
||||
args: {
|
||||
showOrgDetails: true,
|
||||
auditsQuery: mockSuccessResult,
|
||||
filterProps: {
|
||||
...defaultFilterProps,
|
||||
menus: {
|
||||
...defaultFilterProps.menus,
|
||||
organization: MockMenu,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -38,6 +38,7 @@ export interface AuditPageViewProps {
|
||||
error?: unknown;
|
||||
filterProps: ComponentProps<typeof AuditFilter>;
|
||||
auditsQuery: PaginationResult;
|
||||
showOrgDetails: boolean;
|
||||
}
|
||||
|
||||
export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||
@ -47,6 +48,7 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||
error,
|
||||
filterProps,
|
||||
auditsQuery: paginationResult,
|
||||
showOrgDetails,
|
||||
}) => {
|
||||
const isLoading =
|
||||
(auditLogs === undefined || paginationResult.totalRecords === undefined) &&
|
||||
@ -117,7 +119,11 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||
items={auditLogs}
|
||||
getDate={(log) => new Date(log.time)}
|
||||
row={(log) => (
|
||||
<AuditLogRow key={log.id} auditLog={log} />
|
||||
<AuditLogRow
|
||||
key={log.id}
|
||||
auditLog={log}
|
||||
showOrgDetails={showOrgDetails}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
@ -2211,6 +2211,9 @@ export const MockEntitlementsWithUserLimit: TypesGen.Entitlements = {
|
||||
|
||||
export const MockExperiments: TypesGen.Experiment[] = [];
|
||||
|
||||
/**
|
||||
* An audit log for MockOrganization.
|
||||
*/
|
||||
export const MockAuditLog: TypesGen.AuditLog = {
|
||||
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
|
||||
request_id: "53bded77-7b9d-4e82-8771-991a34d759f9",
|
||||
@ -2218,9 +2221,9 @@ export const MockAuditLog: TypesGen.AuditLog = {
|
||||
organization_id: MockOrganization.id,
|
||||
organization: {
|
||||
id: MockOrganization.id,
|
||||
name: "mock name",
|
||||
display_name: "mock display name",
|
||||
icon: "/emojis/1f48f-1f3ff.png",
|
||||
name: MockOrganization.name,
|
||||
display_name: MockOrganization.display_name,
|
||||
icon: MockOrganization.icon,
|
||||
},
|
||||
ip: "127.0.0.1",
|
||||
user_agent:
|
||||
@ -2245,12 +2248,22 @@ export const MockAuditLog: TypesGen.AuditLog = {
|
||||
is_deleted: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* An audit log for MockOrganization2.
|
||||
*/
|
||||
export const MockAuditLog2: TypesGen.AuditLog = {
|
||||
...MockAuditLog,
|
||||
id: "53bded77-7b9d-4e82-8771-991a34d759f9",
|
||||
action: "write",
|
||||
time: "2022-05-20T16:45:57.122Z",
|
||||
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: {
|
||||
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 = {
|
||||
...MockAuditLog,
|
||||
additional_fields: {
|
||||
|
Reference in New Issue
Block a user