Skip to content

feat: add organizations filter to audit table #13978

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 6 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions docs/admin/audit-logs.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions enterprise/audit/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions site/src/components/Filter/SelectFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type SelectFilterProps = {
onSelect: (option: SelectFilterOption | undefined) => void;
// SelectFilterSearch element
selectFilterSearch?: ReactNode;
width?: number;
};

export const SelectFilter: FC<SelectFilterProps> = ({
Expand All @@ -42,6 +43,7 @@ export const SelectFilter: FC<SelectFilterProps> = ({
placeholder,
emptyText,
selectFilterSearch,
width = BASE_WIDTH,
}) => {
const [open, setOpen] = useState(false);

Expand All @@ -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}
Expand All @@ -64,7 +66,7 @@ export const SelectFilter: FC<SelectFilterProps> = ({
// wide as possible.
width: selectFilterSearch ? "100%" : undefined,
maxWidth: POPOVER_WIDTH,
minWidth: BASE_WIDTH,
minWidth: width,
},
}}
>
Expand Down
4 changes: 3 additions & 1 deletion site/src/components/Filter/UserFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -116,6 +117,7 @@ export const UserMenu: FC<UserMenuProps> = ({ menu }) => {
onChange={menu.setQuery}
/>
}
width={width}
/>
);
};
113 changes: 108 additions & 5 deletions site/src/pages/AuditPage/AuditFilter.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 = [
Expand All @@ -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")}
Expand All @@ -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={
Expand Down Expand Up @@ -92,14 +102,20 @@ 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"
placeholder="All actions"
options={menu.searchOptions}
onSelect={menu.selectOption}
selectedOption={menu.selectedOption ?? undefined}
width={width}
/>
);
};
Expand Down Expand Up @@ -146,14 +162,101 @@ 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"
placeholder="All resource types"
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}
/>
);
};
18 changes: 18 additions & 0 deletions site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
21 changes: 20 additions & 1 deletion site/src/pages/AuditPage/AuditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand All @@ -67,13 +82,17 @@ const AuditPage: FC = () => {
isAuditLogVisible={isAuditLogVisible}
auditsQuery={auditsQuery}
error={auditsQuery.error}
showOrgDetails={experiments.includes("multi-organization")}
filterProps={{
filter,
error: auditsQuery.error,
menus: {
user: userMenu,
action: actionMenu,
resourceType: resourceTypeMenu,
organization: experiments.includes("multi-organization")
? organizationsMenu
: undefined,
},
}}
/>
Expand Down
26 changes: 24 additions & 2 deletions site/src/pages/AuditPage/AuditPageView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -21,6 +26,7 @@ const defaultFilterProps = getDefaultFilterProps<FilterProps>({
username: MockUser.username,
action: undefined,
resource_type: undefined,
organization: undefined,
},
menus: {
user: MockMenu,
Expand All @@ -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,
},
};

Expand Down Expand Up @@ -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,
},
},
},
};
Loading
Loading