fix: only show editable orgs on deployment page (#14193)

Also make sure the redirect from /organizations goes to an org that the
user can edit, rather than always the default org.
This commit is contained in:
Asher
2024-08-08 22:18:20 -08:00
committed by GitHub
parent d6c4d47229
commit 6019d0ba96
9 changed files with 538 additions and 180 deletions
+98 -28
View File
@@ -1,6 +1,7 @@
import type { QueryClient } from "react-query";
import { API } from "api/api";
import type {
AuthorizationResponse,
CreateOrganizationRequest,
UpdateOrganizationRequest,
} from "api/typesGenerated";
@@ -133,15 +134,15 @@ export const organizationPermissions = (organizationId: string | undefined) => {
return {
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
// 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.
API.checkAuthorization({
checks: {
viewMembers: {
object: {
resource_type: "organization_member",
organization_id: organizationId,
},
action: "read",
},
editMembers: {
object: {
resource_type: "organization_member",
@@ -156,27 +157,6 @@ export const organizationPermissions = (organizationId: string | undefined) => {
},
action: "create",
},
viewGroups: {
object: {
resource_type: "group",
organization_id: organizationId,
},
action: "read",
},
editOrganization: {
object: {
resource_type: "organization",
organization_id: organizationId,
},
action: "update",
},
auditOrganization: {
object: {
resource_type: "audit_log",
organization_id: organizationId,
},
action: "read",
},
assignOrgRole: {
object: {
resource_type: "assign_org_role",
@@ -188,3 +168,93 @@ export const organizationPermissions = (organizationId: string | undefined) => {
}),
};
};
/**
* Fetch permissions for all provided organizations.
*
* If organizations are undefined, return a disabled query.
*/
export const organizationsPermissions = (
organizationIds: string[] | undefined,
) => {
if (!organizationIds) {
return { enabled: false };
}
return {
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).
const checks = (organizationId: string) => ({
editMembers: {
object: {
resource_type: "organization_member",
organization_id: organizationId,
},
action: "update",
},
editGroups: {
object: {
resource_type: "group",
organization_id: organizationId,
},
action: "update",
},
editOrganization: {
object: {
resource_type: "organization",
organization_id: organizationId,
},
action: "update",
},
auditOrganization: {
object: {
resource_type: "audit_log",
organization_id: organizationId,
},
action: "read",
},
assignOrgRole: {
object: {
resource_type: "assign_org_role",
organization_id: organizationId,
},
action: "create",
},
});
// The endpoint takes a flat array, so to avoid collisions prepend each
// check with the org ID (the key can be anything we want).
const prefixedChecks = organizationIds
.map((orgId) =>
Object.entries(checks(orgId)).map(([key, val]) => [
`${orgId}.${key}`,
val,
]),
)
.flat();
const response = await API.checkAuthorization({
checks: Object.fromEntries(prefixedChecks),
});
// Now we can unflatten by parsing out the org ID from each check.
return Object.entries(response).reduce(
(acc, [key, value]) => {
const index = key.indexOf(".");
const orgId = key.substring(0, index);
const perm = key.substring(index + 1);
if (!acc[orgId]) {
acc[orgId] = {};
}
acc[orgId][perm] = value;
return acc;
},
{} as Record<string, AuthorizationResponse>,
);
},
};
};
@@ -2,7 +2,7 @@ import { type FC, Suspense } from "react";
import { useQuery } from "react-query";
import { Outlet } from "react-router-dom";
import { deploymentConfig } from "api/queries/deployment";
import type { Organization } from "api/typesGenerated";
import type { AuthorizationResponse, Organization } from "api/typesGenerated";
import { Loader } from "components/Loader/Loader";
import { Margins } from "components/Margins/Margins";
import { Stack } from "components/Stack/Stack";
@@ -21,6 +21,20 @@ export const useOrganizationSettings = (): OrganizationSettingsValue => {
return { organizations };
};
/**
* Return true if the user can edit the organization settings or its members.
*/
export const canEditOrganization = (
permissions: AuthorizationResponse | undefined,
) => {
return (
permissions !== undefined &&
(permissions.editOrganization ||
permissions.editMembers ||
permissions.editGroups)
);
};
/**
* A multi-org capable settings page layout.
*
@@ -28,7 +28,6 @@ beforeEach(() => {
http.post("/api/v2/authcheck", async () => {
return HttpResponse.json({
editMembers: true,
viewMembers: true,
viewDeploymentValues: true,
});
}),
@@ -0,0 +1,119 @@
import { screen, within } from "@testing-library/react";
import { HttpResponse, http } from "msw";
import {
MockDefaultOrganization,
MockOrganization2,
} from "testHelpers/entities";
import {
renderWithManagementSettingsLayout,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import { server } from "testHelpers/server";
import OrganizationSettingsPage from "./OrganizationSettingsPage";
jest.spyOn(console, "error").mockImplementation(() => {});
const renderRootPage = async () => {
renderWithManagementSettingsLayout(<OrganizationSettingsPage />, {
route: "/organizations",
path: "/organizations/:organization?",
});
await waitForLoaderToBeRemoved();
};
const renderPage = async (orgName: string) => {
renderWithManagementSettingsLayout(<OrganizationSettingsPage />, {
route: `/organizations/${orgName}`,
path: "/organizations/:organization",
});
await waitForLoaderToBeRemoved();
};
describe("OrganizationSettingsPage", () => {
it("has no organizations", async () => {
server.use(
http.get("/api/v2/organizations", () => {
return HttpResponse.json([]);
}),
http.post("/api/v2/authcheck", async () => {
return HttpResponse.json({
[`${MockDefaultOrganization.id}.editOrganization`]: true,
viewDeploymentValues: true,
});
}),
);
await renderRootPage();
await screen.findByText("No organizations found");
});
it("has no editable organizations", async () => {
server.use(
http.get("/api/v2/organizations", () => {
return HttpResponse.json([MockDefaultOrganization, MockOrganization2]);
}),
http.post("/api/v2/authcheck", async () => {
return HttpResponse.json({
viewDeploymentValues: true,
});
}),
);
await renderRootPage();
await screen.findByText("No organizations found");
});
it("redirects to default organization", async () => {
server.use(
http.get("/api/v2/organizations", () => {
// Default always preferred regardless of order.
return HttpResponse.json([MockOrganization2, MockDefaultOrganization]);
}),
http.post("/api/v2/authcheck", async () => {
return HttpResponse.json({
[`${MockDefaultOrganization.id}.editOrganization`]: true,
[`${MockOrganization2.id}.editOrganization`]: true,
viewDeploymentValues: true,
});
}),
);
await renderRootPage();
const form = screen.getByTestId("org-settings-form");
expect(within(form).getByRole("textbox", { name: "Name" })).toHaveValue(
MockDefaultOrganization.name,
);
});
it("redirects to non-default organization", async () => {
server.use(
http.get("/api/v2/organizations", () => {
return HttpResponse.json([MockDefaultOrganization, MockOrganization2]);
}),
http.post("/api/v2/authcheck", async () => {
return HttpResponse.json({
[`${MockOrganization2.id}.editOrganization`]: true,
viewDeploymentValues: true,
});
}),
);
await renderRootPage();
const form = screen.getByTestId("org-settings-form");
expect(within(form).getByRole("textbox", { name: "Name" })).toHaveValue(
MockOrganization2.name,
);
});
it("cannot find organization", async () => {
server.use(
http.get("/api/v2/organizations", () => {
return HttpResponse.json([MockDefaultOrganization, MockOrganization2]);
}),
http.post("/api/v2/authcheck", async () => {
return HttpResponse.json({
[`${MockOrganization2.id}.editOrganization`]: true,
viewDeploymentValues: true,
});
}),
);
await renderPage("the-endless-void");
await screen.findByText("Organization not found");
});
});
@@ -4,13 +4,16 @@ import { Navigate, useNavigate, useParams } from "react-router-dom";
import {
updateOrganization,
deleteOrganization,
organizationPermissions,
organizationsPermissions,
} from "api/queries/organizations";
import type { Organization } from "api/typesGenerated";
import { EmptyState } from "components/EmptyState/EmptyState";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { useOrganizationSettings } from "./ManagementSettingsLayout";
import {
canEditOrganization,
useOrganizationSettings,
} from "./ManagementSettingsLayout";
import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView";
const OrganizationSettingsPage: FC = () => {
@@ -32,37 +35,42 @@ const OrganizationSettingsPage: FC = () => {
organizations && organizationName
? getOrganizationByName(organizations, organizationName)
: undefined;
const permissionsQuery = useQuery(organizationPermissions(organization?.id));
const permissionsQuery = useQuery(
organizationsPermissions(organizations?.map((o) => o.id)),
);
if (!organizations) {
const permissions = permissionsQuery.data;
if (!organizations || !permissions) {
return <Loader />;
}
// Redirect /organizations => /organizations/default-org
// Redirect /organizations => /organizations/default-org, or if they cannot edit
// the default org, then the first org they can edit, if any.
if (!organizationName) {
const defaultOrg = getOrganizationByDefault(organizations);
if (defaultOrg) {
return <Navigate to={`/organizations/${defaultOrg.name}`} replace />;
const editableOrg = organizations
.sort((a, b) => {
// Prefer default org (it may not be first).
// JavaScript will happily subtract booleans, but use numbers to keep
// the compiler happy.
return (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0);
})
.find((org) => canEditOrganization(permissions[org.id]));
if (editableOrg) {
return <Navigate to={`/organizations/${editableOrg.name}`} replace />;
}
// We expect there to always be a default organization.
throw new Error("No default organization found");
return <EmptyState message="No organizations found" />;
}
if (!organization) {
return <EmptyState message="Organization not found" />;
}
const permissions = permissionsQuery.data;
if (!permissions) {
return <Loader />;
}
const error =
updateOrganizationMutation.error ?? deleteOrganizationMutation.error;
return (
<OrganizationSettingsPageView
canEdit={permissions.editOrganization}
canEdit={permissions[organization.id]?.editOrganization ?? false}
organization={organization}
error={error}
onSubmit={async (values) => {
@@ -85,8 +93,5 @@ const OrganizationSettingsPage: FC = () => {
export default OrganizationSettingsPage;
const getOrganizationByDefault = (organizations: Organization[]) =>
organizations.find((org) => org.is_default);
const getOrganizationByName = (organizations: Organization[], name: string) =>
organizations.find((org) => org.name === name);
@@ -78,6 +78,7 @@ export const OrganizationSettingsPageView: FC<
)}
<HorizontalForm
data-testid="org-settings-form"
onSubmit={form.handleSubmit}
aria-label="Organization settings form"
>
@@ -1,10 +1,13 @@
import type { FC } from "react";
import { useQuery } from "react-query";
import { useParams } from "react-router-dom";
import { organizationPermissions } from "api/queries/organizations";
import { useLocation, useParams } from "react-router-dom";
import { organizationsPermissions } from "api/queries/organizations";
import { useAuthenticated } from "contexts/auth/RequireAuth";
import { useOrganizationSettings } from "./ManagementSettingsLayout";
import { SidebarView } from "./SidebarView";
import {
canEditOrganization,
useOrganizationSettings,
} from "./ManagementSettingsLayout";
import { type OrganizationWithPermissions, SidebarView } from "./SidebarView";
/**
* A combined deployment settings and organization menu.
@@ -14,27 +17,42 @@ import { SidebarView } from "./SidebarView";
* DeploySettingsPage/Sidebar instead.
*/
export const Sidebar: FC = () => {
const location = useLocation();
const { permissions } = useAuthenticated();
const { organizations } = useOrganizationSettings();
const { organization: organizationName } = useParams() as {
organization?: string;
};
// If there is no organization name, the settings page will load, and it will
// redirect to the default organization, so eventually there will always be an
// organization name.
const activeOrganization = organizations?.find(
(o) => o.name === organizationName,
);
const activeOrgPermissionsQuery = useQuery(
organizationPermissions(activeOrganization?.id),
const orgPermissionsQuery = useQuery(
organizationsPermissions(organizations?.map((o) => o.id)),
);
// Sometimes a user can read an organization but cannot actually do anything
// with it. For now, these are filtered out so you only see organizations you
// can manage in some way.
const editableOrgs = organizations
?.map((org) => {
return {
...org,
permissions: orgPermissionsQuery.data?.[org.id],
};
})
// TypeScript is not able to infer whether permissions are defined on the
// object even if we explicitly check org.permissions here, so add the `is`
// here to help out (canEditOrganization does the actual check).
.filter((org): org is OrganizationWithPermissions => {
return canEditOrganization(org.permissions);
});
return (
<SidebarView
activeOrganization={activeOrganization}
activeOrgPermissions={activeOrgPermissionsQuery.data}
organizations={organizations}
// Both activeSettings and activeOrganizationName could be be falsey if
// the user is on /organizations but has no editable organizations to
// which we can redirect.
activeSettings={location.pathname.startsWith("/deployment")}
activeOrganizationName={organizationName}
organizations={editableOrgs}
permissions={permissions}
/>
);
@@ -12,9 +12,28 @@ const meta: Meta<typeof SidebarView> = {
component: SidebarView,
decorators: [withDashboardProvider],
args: {
activeOrganization: undefined,
activeOrgPermissions: undefined,
organizations: [MockOrganization, MockOrganization2],
activeSettings: true,
activeOrganizationName: undefined,
organizations: [
{
...MockOrganization,
permissions: {
editOrganization: true,
editMembers: true,
editGroups: true,
auditOrganization: true,
},
},
{
...MockOrganization2,
permissions: {
editOrganization: true,
editMembers: true,
editGroups: true,
auditOrganization: true,
},
},
],
permissions: MockPermissions,
},
};
@@ -22,7 +41,11 @@ const meta: Meta<typeof SidebarView> = {
export default meta;
type Story = StoryObj<typeof SidebarView>;
export const Default: Story = {};
export const LoadingOrganizations: Story = {
args: {
organizations: undefined,
},
};
export const NoCreateOrg: Story = {
args: {
@@ -76,45 +99,126 @@ export const NoPermissions: Story = {
},
};
export const SelectedOrgLoading: Story = {
export const NoSelected: Story = {
args: {
activeOrganization: MockOrganization,
activeSettings: false,
},
};
export const SelectedOrgNoMatch: Story = {
args: {
activeOrganizationName: MockOrganization.name,
organizations: [],
},
};
export const SelectedOrgAdmin: Story = {
args: {
activeOrganization: MockOrganization,
activeOrgPermissions: {
editOrganization: true,
viewMembers: true,
viewGroups: true,
auditOrganization: true,
assignOrgRole: true,
},
activeOrganizationName: MockOrganization.name,
organizations: [
{
...MockOrganization,
permissions: {
editOrganization: true,
editMembers: true,
editGroups: true,
auditOrganization: true,
assignOrgRole: true,
},
},
],
},
};
export const SelectedOrgAuditor: Story = {
args: {
activeOrganization: MockOrganization,
activeOrgPermissions: {
editOrganization: false,
viewMembers: false,
viewGroups: false,
auditOrganization: true,
activeOrganizationName: MockOrganization.name,
permissions: {
...MockPermissions,
createOrganization: false,
},
organizations: [
{
...MockOrganization,
permissions: {
editOrganization: false,
editMembers: false,
editGroups: false,
auditOrganization: true,
},
},
],
},
};
export const SelectedOrgNoPerms: Story = {
export const SelectedOrgUserAdmin: Story = {
args: {
activeOrganization: MockOrganization,
activeOrgPermissions: {
editOrganization: false,
viewMembers: false,
viewGroups: false,
auditOrganization: false,
activeOrganizationName: MockOrganization.name,
permissions: {
...MockPermissions,
createOrganization: false,
},
organizations: [
{
...MockOrganization,
permissions: {
editOrganization: false,
editMembers: true,
editGroups: true,
auditOrganization: false,
},
},
],
},
};
export const MultiOrgAdminAndUserAdmin: Story = {
args: {
organizations: [
{
...MockOrganization,
permissions: {
editOrganization: false,
editMembers: false,
editGroups: false,
auditOrganization: true,
},
},
{
...MockOrganization2,
permissions: {
editOrganization: false,
editMembers: true,
editGroups: true,
auditOrganization: false,
},
},
],
},
};
export const SelectedMultiOrgAdminAndUserAdmin: Story = {
args: {
activeOrganizationName: MockOrganization2.name,
organizations: [
{
...MockOrganization,
permissions: {
editOrganization: false,
editMembers: false,
editGroups: false,
auditOrganization: true,
},
},
{
...MockOrganization2,
permissions: {
editOrganization: false,
editMembers: true,
editGroups: true,
auditOrganization: false,
},
},
],
},
};
@@ -13,19 +13,17 @@ import { type ClassName, useClassName } from "hooks/useClassName";
import { useDashboard } from "modules/dashboard/useDashboard";
import { linkToAuditing, linkToUsers, withFilter } from "modules/navigation";
export interface OrganizationWithPermissions extends Organization {
permissions: AuthorizationResponse;
}
interface SidebarProps {
/**
* The active org if an org is being viewed. If there is no active
* organization, assume one of the deployment settings pages are being viewed.
*/
activeOrganization: Organization | undefined;
/**
* The permissions for the active org or undefined if still fetching (or if
* there is no active org).
*/
activeOrgPermissions: AuthorizationResponse | undefined;
/** The list of organizations or undefined if still fetching. */
organizations: Organization[] | undefined;
/** True if a settings page is being viewed. */
activeSettings: boolean;
/** The active org name, if any. Overrides activeSettings. */
activeOrganizationName: string | undefined;
/** Organizations and their permissions or undefined if still fetching. */
organizations: OrganizationWithPermissions[] | undefined;
/** Site-wide permissions. */
permissions: AuthorizationResponse;
}
@@ -33,44 +31,25 @@ interface SidebarProps {
/**
* A combined deployment settings and organization menu.
*/
export const SidebarView: FC<SidebarProps> = (props) => {
export const SidebarView: FC<SidebarProps> = ({
activeSettings,
activeOrganizationName,
organizations,
permissions,
}) => {
// TODO: Do something nice to scroll to the active org.
return (
<BaseSidebar>
<header css={styles.sidebarHeader}>Deployment</header>
<DeploymentSettingsNavigation
active={!props.activeOrganization}
permissions={props.permissions}
active={!activeOrganizationName && activeSettings}
permissions={permissions}
/>
<OrganizationsSettingsNavigation
activeOrganizationName={activeOrganizationName}
organizations={organizations}
permissions={permissions}
/>
{props.organizations ? (
<>
<header css={styles.sidebarHeader}>Organizations</header>
{props.permissions.createOrganization && (
<SidebarNavItem
active="auto"
href="/organizations/new"
icon={<AddIcon />}
>
New organization
</SidebarNavItem>
)}
{props.organizations.map((org) => {
const orgActive =
Boolean(props.activeOrganization) &&
org.name === props.activeOrganization?.name;
return (
<OrganizationSettingsNavigation
key={org.id}
organization={org}
permissions={orgActive ? props.activeOrgPermissions : undefined}
active={orgActive}
/>
);
})}
</>
) : (
<Loader />
)}
</BaseSidebar>
);
};
@@ -89,15 +68,16 @@ interface DeploymentSettingsNavigationProps {
* Menu items are shown based on the permissions. If organizations can be
* viewed, groups are skipped since they will show under each org instead.
*/
const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = (
props,
) => {
const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
active,
permissions,
}) => {
return (
<div css={{ paddingBottom: 12 }}>
<SidebarNavItem
active={props.active}
active={active}
href={
props.permissions.viewDeploymentValues
permissions.viewDeploymentValues
? "/deployment/general"
: "/deployment/workspace-proxies"
}
@@ -108,23 +88,23 @@ const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = (
>
Deployment
</SidebarNavItem>
{props.active && (
{active && (
<Stack spacing={0.5} css={{ marginBottom: 8, marginTop: 8 }}>
{props.permissions.viewDeploymentValues && (
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="general">General</SidebarNavSubItem>
)}
{props.permissions.viewAllLicenses && (
{permissions.viewAllLicenses && (
<SidebarNavSubItem href="licenses">Licenses</SidebarNavSubItem>
)}
{props.permissions.editDeploymentValues && (
{permissions.editDeploymentValues && (
<SidebarNavSubItem href="appearance">Appearance</SidebarNavSubItem>
)}
{props.permissions.viewDeploymentValues && (
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="userauth">
User Authentication
</SidebarNavSubItem>
)}
{props.permissions.viewDeploymentValues && (
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="external-auth">
External Authentication
</SidebarNavSubItem>
@@ -133,27 +113,27 @@ const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = (
<SidebarNavSubItem href="oauth2-provider/ap>
OAuth2 Applications
</SidebarNavSubItem>*/}
{props.permissions.viewDeploymentValues && (
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="network">Network</SidebarNavSubItem>
)}
{/* All users can view workspace regions. */}
<SidebarNavSubItem href="workspace-proxies">
Workspace Proxies
</SidebarNavSubItem>
{props.permissions.viewDeploymentValues && (
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="security">Security</SidebarNavSubItem>
)}
{props.permissions.viewDeploymentValues && (
{permissions.viewDeploymentValues && (
<SidebarNavSubItem href="observability">
Observability
</SidebarNavSubItem>
)}
{props.permissions.viewAllUsers && (
{permissions.viewAllUsers && (
<SidebarNavSubItem href={linkToUsers.slice(1)}>
Users
</SidebarNavSubItem>
)}
{props.permissions.viewAnyAuditLog && (
{permissions.viewAnyAuditLog && (
<SidebarNavSubItem href={linkToAuditing.slice(1)}>
Auditing
</SidebarNavSubItem>
@@ -168,70 +148,118 @@ function urlForSubpage(organizationName: string, subpage: string = ""): string {
return `/organizations/${organizationName}/${subpage}`;
}
interface OrganizationSettingsNavigationProps {
active: boolean;
organization: Organization;
permissions: AuthorizationResponse | undefined;
interface OrganizationsSettingsNavigationProps {
/** The active org name if an org is being viewed. */
activeOrganizationName: string | undefined;
/** Organizations and their permissions or undefined if still fetching. */
organizations: OrganizationWithPermissions[] | undefined;
/** Site-wide permissions. */
permissions: AuthorizationResponse;
}
/**
* Displays navigation for an organization.
* Displays navigation for all organizations and a create organization link.
*
* If organizations or their permissions are still loading, show a loader.
*
* If there are no organizations and the user does not have the create org
* permission, nothing is displayed.
*/
const OrganizationsSettingsNavigation: FC<
OrganizationsSettingsNavigationProps
> = ({ activeOrganizationName, organizations, permissions }) => {
// Wait for organizations and their permissions to load in.
if (!organizations) {
return <Loader />;
}
if (organizations.length <= 0 && !permissions.createOrganization) {
return null;
}
return (
<>
<header css={styles.sidebarHeader}>Organizations</header>
{permissions.createOrganization && (
<SidebarNavItem
active="auto"
href="/organizations/new"
icon={<AddIcon />}
>
New organization
</SidebarNavItem>
)}
{organizations.map((org) => (
<OrganizationSettingsNavigation
key={org.id}
organization={org}
active={org.name === activeOrganizationName}
/>
))}
</>
);
};
interface OrganizationSettingsNavigationProps {
/** Whether this organization is currently selected. */
active: boolean;
/** The organization to display in the navigation. */
organization: OrganizationWithPermissions;
}
/**
* Displays navigation for a single organization.
*
* If inactive, no sub-menu items will be shown, just the organization name.
*
* If active, it will show a loader until the permissions are defined, then the
* sub-menu items are shown as appropriate.
* If active, it will show sub-menu items based on the permissions.
*/
const OrganizationSettingsNavigation: FC<
OrganizationSettingsNavigationProps
> = (props) => {
> = ({ active, organization }) => {
const { experiments } = useDashboard();
return (
<>
<SidebarNavItem
active={props.active}
href={urlForSubpage(props.organization.name)}
active={active}
href={urlForSubpage(organization.name)}
icon={
<UserAvatar
key={props.organization.id}
key={organization.id}
size="sm"
username={props.organization.display_name}
avatarURL={props.organization.icon}
username={organization.display_name}
avatarURL={organization.icon}
/>
}
>
{props.organization.display_name}
{organization.display_name}
</SidebarNavItem>
{props.active && !props.permissions && <Loader />}
{props.active && props.permissions && (
{active && (
<Stack spacing={0.5} css={{ marginBottom: 8, marginTop: 8 }}>
{props.permissions.editOrganization && (
<SidebarNavSubItem
end
href={urlForSubpage(props.organization.name)}
>
{organization.permissions.editOrganization && (
<SidebarNavSubItem end href={urlForSubpage(organization.name)}>
Organization settings
</SidebarNavSubItem>
)}
{props.permissions.viewMembers && (
{organization.permissions.editMembers && (
<SidebarNavSubItem
href={urlForSubpage(props.organization.name, "members")}
href={urlForSubpage(organization.name, "members")}
>
Members
</SidebarNavSubItem>
)}
{props.permissions.viewGroups && (
{organization.permissions.editGroups && (
<SidebarNavSubItem
href={urlForSubpage(props.organization.name, "groups")}
href={urlForSubpage(organization.name, "groups")}
>
Groups
</SidebarNavSubItem>
)}
{props.permissions.assignOrgRole &&
{organization.permissions.assignOrgRole &&
experiments.includes("custom-roles") && (
<SidebarNavSubItem
href={urlForSubpage(props.organization.name, "roles")}
href={urlForSubpage(organization.name, "roles")}
>
Roles
</SidebarNavSubItem>
@@ -239,11 +267,11 @@ const OrganizationSettingsNavigation: FC<
{/* 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. */}
{props.permissions.auditOrganization && (
{organization.permissions.auditOrganization && (
<SidebarNavSubItem
href={`/deployment${withFilter(
linkToAuditing,
`organization:${props.organization.name}`,
`organization:${organization.name}`,
)}`}
>
Auditing