mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: move back to single audit log page (#14212)
* chore: remove per-org audit links For now at least, we will have the one audit page at /audit which lets you filter by organization. This also removes the need to do per-org audit permission checks. * Filter audit org dropdown by auditable orgs Previously all orgs you can list would appear, but you might not be able to audit all of them.
This commit is contained in:
@@ -135,9 +135,9 @@ export const organizationPermissions = (organizationId: string | undefined) => {
|
||||
queryKey: ["organization", organizationId, "permissions"],
|
||||
queryFn: () =>
|
||||
// Only request what we use on individual org settings, members, and group
|
||||
// pages, which at the moment is whether you can edit the members or roles
|
||||
// on the members page and whether you can see the create group button on
|
||||
// the groups page. The edit organization check for the settings page is
|
||||
// pages, which at the moment is whether you can edit the members on the
|
||||
// members page, create roles on the roles page, and create groups on the
|
||||
// groups page. The edit organization check for the settings page is
|
||||
// covered by the multi-org query at the moment, and the edit group check
|
||||
// on the group page is done on the group itself, not the org, so neither
|
||||
// show up here.
|
||||
@@ -185,9 +185,9 @@ export const organizationsPermissions = (
|
||||
queryKey: ["organizations", organizationIds.sort(), "permissions"],
|
||||
queryFn: async () => {
|
||||
// Only request what we need for the sidebar, which is one edit permission
|
||||
// per sub-link (audit, settings, groups, roles, and members pages) that
|
||||
// tells us whether to show that page, since we only show them if you can
|
||||
// edit (and not, at the moment if you can only view).
|
||||
// per sub-link (settings, groups, roles, and members pages) that tells us
|
||||
// whether to show that page, since we only show them if you can edit (and
|
||||
// not, at the moment if you can only view).
|
||||
const checks = (organizationId: string) => ({
|
||||
editMembers: {
|
||||
object: {
|
||||
@@ -210,13 +210,6 @@ export const organizationsPermissions = (
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
auditOrganization: {
|
||||
object: {
|
||||
resource_type: "audit_log",
|
||||
organization_id: organizationId,
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
assignOrgRole: {
|
||||
object: {
|
||||
resource_type: "assign_org_role",
|
||||
|
||||
@@ -6,6 +6,7 @@ import Divider from "@mui/material/Divider";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Skeleton, { type SkeletonProps } from "@mui/material/Skeleton";
|
||||
import type { Breakpoint } from "@mui/system/createTheme";
|
||||
import { type FC, type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import type { useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
@@ -142,8 +143,7 @@ type FilterProps = {
|
||||
error?: unknown;
|
||||
options?: ReactNode;
|
||||
presets: PresetFilter[];
|
||||
/** Set to true if there is not much horizontal space. */
|
||||
compact?: boolean;
|
||||
breakpoint?: Breakpoint;
|
||||
};
|
||||
|
||||
export const Filter: FC<FilterProps> = ({
|
||||
@@ -156,7 +156,7 @@ export const Filter: FC<FilterProps> = ({
|
||||
learnMoreLabel2,
|
||||
learnMoreLink2,
|
||||
presets,
|
||||
compact,
|
||||
breakpoint = "md",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
// Storing local copy of the filter query so that it can be updated more
|
||||
@@ -187,12 +187,9 @@ export const Filter: FC<FilterProps> = ({
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
// 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",
|
||||
flexWrap: "nowrap",
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
[theme.breakpoints.down(breakpoint)]: {
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -124,11 +124,7 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
|
||||
{canViewAuditLog && (
|
||||
<MenuItem
|
||||
component={NavLink}
|
||||
to={
|
||||
canViewOrganizations
|
||||
? `/deployment${linkToAuditing}`
|
||||
: linkToAuditing
|
||||
}
|
||||
to={linkToAuditing}
|
||||
css={styles.menuItem}
|
||||
onClick={onPopoverClose}
|
||||
>
|
||||
|
||||
@@ -51,6 +51,8 @@ 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")}
|
||||
@@ -58,16 +60,14 @@ 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)}
|
||||
breakpoint={menus.organization && "lg"}
|
||||
options={
|
||||
<>
|
||||
<ResourceTypeMenu menu={menus.resourceType} />
|
||||
<ActionMenu menu={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 menu={menus.organization} />
|
||||
<OrganizationsMenu width={width} menu={menus.organization} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
@@ -211,19 +211,36 @@ export const useOrganizationsFilterMenu = ({
|
||||
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}
|
||||
/>
|
||||
// Only show the organizations for which you can view audit logs.
|
||||
const organizations = await API.getOrganizations();
|
||||
const permissions = await API.checkAuthorization({
|
||||
checks: Object.fromEntries(
|
||||
organizations.map((organization) => [
|
||||
organization.id,
|
||||
{
|
||||
object: {
|
||||
resource_type: "audit_log",
|
||||
organization_id: organization.id,
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
]),
|
||||
),
|
||||
}));
|
||||
});
|
||||
return organizations
|
||||
.filter((organization) => permissions[organization.id])
|
||||
.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}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FC } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useSearchParams, Navigate, useLocation } from "react-router-dom";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { paginatedAudits } from "api/queries/audits";
|
||||
import { useFilter } from "components/Filter/filter";
|
||||
import { useUserFilterMenu } from "components/Filter/UserFilter";
|
||||
@@ -19,7 +19,6 @@ import { AuditPageView } from "./AuditPageView";
|
||||
const AuditPage: FC = () => {
|
||||
const feats = useFeatureVisibility();
|
||||
const { experiments } = useDashboard();
|
||||
const location = useLocation();
|
||||
|
||||
/**
|
||||
* There is an implicit link between auditsQuery and filter via the
|
||||
@@ -71,14 +70,9 @@ 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).
|
||||
const canViewOrganizations =
|
||||
feats.multiple_organizations && experiments.includes("multi-organization");
|
||||
if (canViewOrganizations && location.pathname !== "/deployment/audit") {
|
||||
return <Navigate to={`/deployment/audit${location.search}`} replace />;
|
||||
}
|
||||
// With the multi-organization experiment enabled, show extra organization
|
||||
// info and the organization filter dropdon.
|
||||
const canViewOrganizations = experiments.includes("multi-organization");
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -57,20 +57,8 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||
const isEmpty = !isLoading && auditLogs?.length === 0;
|
||||
|
||||
return (
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
<Margins>
|
||||
<PageHeader>
|
||||
<PageHeaderTitle>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>{Language.title}</span>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Stack } from "components/Stack/Stack";
|
||||
import { UserAvatar } from "components/UserAvatar/UserAvatar";
|
||||
import { type ClassName, useClassName } from "hooks/useClassName";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { linkToAuditing, linkToUsers, withFilter } from "modules/navigation";
|
||||
import { linkToUsers } from "modules/navigation";
|
||||
|
||||
export interface OrganizationWithPermissions extends Organization {
|
||||
permissions: AuthorizationResponse;
|
||||
@@ -133,11 +133,6 @@ const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
|
||||
Users
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
{permissions.viewAnyAuditLog && (
|
||||
<SidebarNavSubItem href={linkToAuditing.slice(1)}>
|
||||
Auditing
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
@@ -264,19 +259,6 @@ const OrganizationSettingsNavigation: FC<
|
||||
Roles
|
||||
</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. */}
|
||||
{organization.permissions.auditOrganization && (
|
||||
<SidebarNavSubItem
|
||||
href={`/deployment${withFilter(
|
||||
linkToAuditing,
|
||||
`organization:${organization.name}`,
|
||||
)}`}
|
||||
>
|
||||
Auditing
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -389,7 +389,6 @@ export const router = createBrowserRouter(
|
||||
<Route path="create" element={<CreateEditRolePage />} />
|
||||
<Route path=":roleName" element={<CreateEditRolePage />} />
|
||||
</Route>
|
||||
<Route path="auditing" element={<></>} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -423,7 +422,6 @@ 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 />}>
|
||||
|
||||
Reference in New Issue
Block a user