chore: embed audit log in deployment settings page (#14023)

* Move audit page to /deployment/audit

The existing link remains but will redirect to the new URL.
If multi-org is not enabled, nothing changes.

* Redirect organization audit page to site-wide audit page

* Always wrap audit log filters when in deployment settings

Otherwise the input is teeny tiny and barely fits a few characters.
Normally we only wrap when the screen shrinks.  Again, no change
if multi-org is not enabled.

This also makes the filter menus take up available space when
wrapping (*does* apply to non-multi-org setups as well).

* Show audit log details in a tooltip

If multi-org is not enabled, details continue to be shown inline.
This commit is contained in:
Asher
2024-07-26 14:12:24 -08:00
committed by GitHub
parent eacdfb9f9c
commit 712662d014
11 changed files with 158 additions and 57 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ export const SelectFilter: FC<SelectFilterProps> = ({
<SelectMenuTrigger>
<SelectMenuButton
startIcon={selectedOption?.startIcon}
css={{ width }}
css={{ width, flexGrow: 1 }}
aria-label={label}
>
{selectedOption?.label ?? placeholder}
+7 -1
View File
@@ -142,6 +142,8 @@ type FilterProps = {
error?: unknown;
options?: ReactNode;
presets: PresetFilter[];
/** Set to true if there is not much horizontal space. */
compact?: boolean;
};
export const Filter: FC<FilterProps> = ({
@@ -154,6 +156,7 @@ export const Filter: FC<FilterProps> = ({
learnMoreLabel2,
learnMoreLink2,
presets,
compact,
}) => {
const theme = useTheme();
// Storing local copy of the filter query so that it can be updated more
@@ -184,7 +187,10 @@ export const Filter: FC<FilterProps> = ({
display: "flex",
gap: 8,
marginBottom: 16,
flexWrap: "nowrap",
// For now compact just means immediately wrapping, but maybe we should
// have a collapsible section or consolidate into one menu or something.
// TODO: Remove separate compact mode once multi-org is stable.
flexWrap: compact ? "wrap" : "nowrap",
[theme.breakpoints.down("md")]: {
flexWrap: "wrap",
@@ -10,7 +10,7 @@ import {
PopoverTrigger,
usePopover,
} from "components/Popover/Popover";
import { USERS_LINK } from "modules/navigation";
import { AUDIT_LINK, USERS_LINK } from "modules/navigation";
interface DeploymentDropdownProps {
canViewDeployment: boolean;
@@ -114,7 +114,7 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
{canViewAllUsers && (
<MenuItem
component={NavLink}
to={USERS_LINK}
to={canViewOrganizations ? `/deployment${USERS_LINK}` : USERS_LINK}
css={styles.menuItem}
onClick={onPopoverClose}
>
@@ -124,7 +124,7 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
{canViewAuditLog && (
<MenuItem
component={NavLink}
to="/audit"
to={canViewOrganizations ? `/deployment${AUDIT_LINK}` : AUDIT_LINK}
css={styles.menuItem}
onClick={onPopoverClose}
>
+7 -3
View File
@@ -2,6 +2,10 @@
* @fileoverview TODO: centralize navigation code here! URL constants, URL formatting, all of it
*/
export const USERS_LINK = `/users?filter=${encodeURIComponent(
"status:active",
)}`;
export function withFilter(path: string, filter: string) {
return path + (filter ? `?filter=${encodeURIComponent(filter)}` : "");
}
export const AUDIT_LINK = "/audit";
export const USERS_LINK = withFilter("/users", "status:active");
+7 -6
View File
@@ -51,8 +51,6 @@ interface AuditFilterProps {
}
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")}
@@ -60,13 +58,16 @@ export const AuditFilter: FC<AuditFilterProps> = ({ filter, error, menus }) => {
isLoading={menus.user.isInitializing}
filter={filter}
error={error}
// There is not much space with the sidebar and four filters, so in this
// case we will use the compact mode.
compact={Boolean(menus.organization)}
options={
<>
<ResourceTypeMenu width={width} menu={menus.resourceType} />
<ActionMenu width={width} menu={menus.action} />
<UserMenu width={width} menu={menus.user} />
<ResourceTypeMenu menu={menus.resourceType} />
<ActionMenu menu={menus.action} />
<UserMenu menu={menus.user} />
{menus.organization && (
<OrganizationsMenu width={width} menu={menus.organization} />
<OrganizationsMenu menu={menus.organization} />
)}
</>
}
@@ -113,3 +113,10 @@ export const SecretDiffValue: Story = {
auditLog: MockAuditLogGitSSH,
},
};
export const WithOrganization: Story = {
args: {
auditLog: MockAuditLog,
showOrgDetails: true,
},
};
@@ -1,7 +1,9 @@
import type { CSSObject, Interpolation, Theme } from "@emotion/react";
import InfoOutlined from "@mui/icons-material/InfoOutlined";
import Collapse from "@mui/material/Collapse";
import Link from "@mui/material/Link";
import TableCell from "@mui/material/TableCell";
import Tooltip from "@mui/material/Tooltip";
import { type FC, useState } from "react";
import { Link as RouterLink } from "react-router-dom";
import userAgentParser from "ua-parser-js";
@@ -115,42 +117,80 @@ export const AuditLogRow: FC<AuditLogRowProps> = ({
</Stack>
<Stack direction="row" alignItems="center">
<Stack direction="row" spacing={1} alignItems="baseline">
{auditLog.ip && (
<span css={styles.auditLogInfo}>
<>IP: </>
<strong>{auditLog.ip}</strong>
</span>
)}
{os.name && (
<span css={styles.auditLogInfo}>
<>OS: </>
<strong>{os.name}</strong>
</span>
)}
{browser.name && (
<span css={styles.auditLogInfo}>
<>Browser: </>
<strong>
{browser.name} {browser.version}
</strong>
</span>
)}
{showOrgDetails && auditLog.organization && (
<span css={styles.auditLogInfo}>
<>Org: </>
<Link
component={RouterLink}
to={`/organizations/${auditLog.organization.name}`}
>
{/* With multi-org, there is not enough space so show
everything in a tooltip. */}
{showOrgDetails ? (
<Tooltip
title={
<div css={styles.auditLogInfoTooltip}>
{auditLog.ip && (
<div>
<h4 css={styles.auditLogInfoHeader}>IP:</h4>
<div>{auditLog.ip}</div>
</div>
)}
{os.name && (
<div>
<h4 css={styles.auditLogInfoHeader}>OS:</h4>
<div>{os.name}</div>
</div>
)}
{browser.name && (
<div>
<h4 css={styles.auditLogInfoHeader}>Browser:</h4>
<div>
{browser.name} {browser.version}
</div>
</div>
)}
{auditLog.organization && (
<div>
<h4 css={styles.auditLogInfoHeader}>
Organization:
</h4>
<Link
component={RouterLink}
to={`/organizations/${auditLog.organization.name}`}
>
{auditLog.organization.display_name ||
auditLog.organization.name}
</Link>
</div>
)}
</div>
}
>
<InfoOutlined
css={(theme) => ({
fontSize: 20,
color: theme.palette.info.light,
})}
/>
</Tooltip>
) : (
<Stack direction="row" spacing={1} alignItems="baseline">
{auditLog.ip && (
<span css={styles.auditLogInfo}>
<span>IP: </span>
<strong>{auditLog.ip}</strong>
</span>
)}
{os.name && (
<span css={styles.auditLogInfo}>
<span>OS: </span>
<strong>{os.name}</strong>
</span>
)}
{browser.name && (
<span css={styles.auditLogInfo}>
<span>Browser: </span>
<strong>
{auditLog.organization.display_name ||
auditLog.organization.name}
{browser.name} {browser.version}
</strong>
</Link>
</span>
)}
</Stack>
</span>
)}
</Stack>
)}
<Pill
css={styles.httpStatusPill}
@@ -212,6 +252,20 @@ const styles = {
display: "block",
}),
auditLogInfoHeader: (theme) => ({
margin: 0,
color: theme.palette.text.primary,
fontSize: 14,
lineHeight: "150%",
fontWeight: 600,
}),
auditLogInfoTooltip: {
display: "flex",
flexDirection: "column",
gap: 8,
},
// offset the absence of the arrow icon on diff-less logs
columnWithoutDiff: {
marginLeft: "24px",
+12 -5
View File
@@ -1,6 +1,6 @@
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useSearchParams } from "react-router-dom";
import { useSearchParams, Navigate, useLocation } from "react-router-dom";
import { paginatedAudits } from "api/queries/audits";
import { useFilter } from "components/Filter/filter";
import { useUserFilterMenu } from "components/Filter/UserFilter";
@@ -19,6 +19,8 @@ import { AuditPageView } from "./AuditPageView";
const AuditPage: FC = () => {
const { audit_log: isAuditLogVisible } = useFeatureVisibility();
const { experiments } = useDashboard();
const location = useLocation();
const isMultiOrg = experiments.includes("multi-organization");
/**
* There is an implicit link between auditsQuery and filter via the
@@ -70,6 +72,13 @@ const AuditPage: FC = () => {
}),
});
// TODO: Once multi-org is stable, we should place this redirect into the
// router directly, if we still need to maintain it (for users who are
// typing the old URL manually or have it bookmarked).
if (isMultiOrg && location.pathname !== "/deployment/audit") {
return <Navigate to={`/deployment/audit${location.search}`} replace />;
}
return (
<>
<Helmet>
@@ -82,7 +91,7 @@ const AuditPage: FC = () => {
isAuditLogVisible={isAuditLogVisible}
auditsQuery={auditsQuery}
error={auditsQuery.error}
showOrgDetails={experiments.includes("multi-organization")}
showOrgDetails={isMultiOrg}
filterProps={{
filter,
error: auditsQuery.error,
@@ -90,9 +99,7 @@ const AuditPage: FC = () => {
user: userMenu,
action: actionMenu,
resourceType: resourceTypeMenu,
organization: experiments.includes("multi-organization")
? organizationsMenu
: undefined,
organization: isMultiOrg ? organizationsMenu : undefined,
},
}}
/>
+14 -2
View File
@@ -57,8 +57,20 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
const isEmpty = !isLoading && auditLogs?.length === 0;
return (
<Margins>
<PageHeader>
<Margins
css={{
// When acting as a deployment settings page, there is already padding.
// TODO: When multi-org is stable we should move this to the deployment
// settings dir, use the standard header, and remove these margins.
padding: showOrgDetails ? 0 : undefined,
}}
>
<PageHeader
css={{
// When acting as a deployment settings page, there is already padding.
paddingTop: showOrgDetails ? 0 : undefined,
}}
>
<PageHeaderTitle>
<Stack direction="row" spacing={1} alignItems="center">
<span>{Language.title}</span>
@@ -10,7 +10,7 @@ import { Stack } from "components/Stack/Stack";
import { UserAvatar } from "components/UserAvatar/UserAvatar";
import { type ClassName, useClassName } from "hooks/useClassName";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { USERS_LINK } from "modules/navigation";
import { AUDIT_LINK, USERS_LINK, withFilter } from "modules/navigation";
import { useOrganizationSettings } from "./ManagementSettingsLayout";
export const Sidebar: FC = () => {
@@ -103,6 +103,9 @@ const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
{!organizationsEnabled && (
<SidebarNavSubItem href="groups">Groups</SidebarNavSubItem>
)}
<SidebarNavSubItem href={AUDIT_LINK.slice(1)}>
Auditing
</SidebarNavSubItem>
</Stack>
)}
</div>
@@ -148,8 +151,14 @@ export const OrganizationSettingsNavigation: FC<
<SidebarNavSubItem href={urlForSubpage(organization.name, "groups")}>
Groups
</SidebarNavSubItem>
{/* For now redirect to the site-wide audit page with the organization
pre-filled into the filter. Based on user feedback we might want
to serve a copy of the audit page or even delete this link. */}
<SidebarNavSubItem
href={urlForSubpage(organization.name, "auditing")}
href={`/deployment${withFilter(
AUDIT_LINK,
`organization:${organization.name}`,
)}`}
>
Auditing
</SidebarNavSubItem>
+1
View File
@@ -406,6 +406,7 @@ export const router = createBrowserRouter(
<Route path="users" element={<UsersPage />} />
<Route path="users/create" element={<CreateUserPage />} />
{groupsRouter()}
<Route path="audit" element={<AuditPage />} />
</Route>
<Route path="/settings" element={<UserSettingsLayout />}>