chore: perform several small frontend permissions refactors (#16735)

This commit is contained in:
ケイラ
2025-03-07 10:33:09 -07:00
committed by GitHub
parent 54745b1d3f
commit 092c129de0
57 changed files with 158 additions and 174 deletions

View File

@ -167,8 +167,6 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
})
return
}
// TODO: It would be nice to enforce this at the schema level
// but unfortunately our org_members table does not have an ID.
_, err := database.ExpectOne(api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{
OrganizationID: group.OrganizationID,
UserID: uuid.MustParse(id),

View File

@ -20,10 +20,10 @@ export const defaultPassword = "SomeSecurePassword!";
// Credentials for users
export const users = {
admin: {
username: "admin",
owner: {
username: "owner",
password: defaultPassword,
email: "admin@coder.com",
email: "owner@coder.com",
},
templateAdmin: {
username: "template-admin",
@ -41,7 +41,7 @@ export const users = {
username: "auditor",
password: defaultPassword,
email: "auditor@coder.com",
roles: ["Template Admin", "Auditor"],
roles: ["Auditor"],
},
member: {
username: "member",

View File

@ -67,7 +67,7 @@ export type LoginOptions = {
password: string;
};
export async function login(page: Page, options: LoginOptions = users.admin) {
export async function login(page: Page, options: LoginOptions = users.owner) {
const ctx = page.context();
// biome-ignore lint/suspicious/noExplicitAny: reset the current user
(ctx as any)[Symbol.for("currentUser")] = undefined;

View File

@ -16,8 +16,8 @@ test("setup deployment", async ({ page }) => {
}
// Setup first user
await page.getByLabel(Language.emailLabel).fill(users.admin.email);
await page.getByLabel(Language.passwordLabel).fill(users.admin.password);
await page.getByLabel(Language.emailLabel).fill(users.owner.email);
await page.getByLabel(Language.passwordLabel).fill(users.owner.password);
await page.getByTestId("create").click();
await expectUrl(page).toHavePathName("/workspaces");
@ -25,7 +25,7 @@ test("setup deployment", async ({ page }) => {
for (const user of Object.values(users)) {
// Already created as first user
if (user.username === "admin") {
if (user.username === "owner") {
continue;
}

View File

@ -13,19 +13,17 @@ test.describe.configure({ mode: "parallel" });
test.beforeEach(async ({ page }) => {
beforeCoderTest(page);
await login(page, users.auditor);
});
async function resetSearch(page: Page) {
async function resetSearch(page: Page, username: string) {
const clearButton = page.getByLabel("Clear search");
if (await clearButton.isVisible()) {
await clearButton.click();
}
// Filter by the auditor test user to prevent race conditions
const user = currentUser(page);
await expect(page.getByText("All users")).toBeVisible();
await page.getByPlaceholder("Search...").fill(`username:${user.username}`);
await page.getByPlaceholder("Search...").fill(`username:${username}`);
await expect(page.getByText("All users")).not.toBeVisible();
}
@ -33,12 +31,14 @@ test("logins are logged", async ({ page }) => {
requiresLicense();
// Go to the audit history
await login(page, users.auditor);
await page.goto("/audit");
const username = users.auditor.username;
const user = currentUser(page);
const loginMessage = `${user.username} logged in`;
const loginMessage = `${username} logged in`;
// Make sure those things we did all actually show up
await resetSearch(page);
await resetSearch(page, username);
await expect(page.getByText(loginMessage).first()).toBeVisible();
});
@ -46,29 +46,30 @@ test("creating templates and workspaces is logged", async ({ page }) => {
requiresLicense();
// Do some stuff that should show up in the audit logs
await login(page, users.templateAdmin);
const username = users.templateAdmin.username;
const templateName = await createTemplate(page);
const workspaceName = await createWorkspace(page, templateName);
// Go to the audit history
await login(page, users.auditor);
await page.goto("/audit");
const user = currentUser(page);
// Make sure those things we did all actually show up
await resetSearch(page);
await resetSearch(page, username);
await expect(
page.getByText(`${user.username} created template ${templateName}`),
page.getByText(`${username} created template ${templateName}`),
).toBeVisible();
await expect(
page.getByText(`${user.username} created workspace ${workspaceName}`),
page.getByText(`${username} created workspace ${workspaceName}`),
).toBeVisible();
await expect(
page.getByText(`${user.username} started workspace ${workspaceName}`),
page.getByText(`${username} started workspace ${workspaceName}`),
).toBeVisible();
// Make sure we can inspect the details of the log item
const createdWorkspace = page.locator(".MuiTableRow-root", {
hasText: `${user.username} created workspace ${workspaceName}`,
hasText: `${username} created workspace ${workspaceName}`,
});
await createdWorkspace.getByLabel("open-dropdown").click();
await expect(
@ -83,18 +84,19 @@ test("inspecting and filtering audit logs", async ({ page }) => {
requiresLicense();
// Do some stuff that should show up in the audit logs
await login(page, users.templateAdmin);
const username = users.templateAdmin.username;
const templateName = await createTemplate(page);
const workspaceName = await createWorkspace(page, templateName);
// Go to the audit history
await login(page, users.auditor);
await page.goto("/audit");
const user = currentUser(page);
const loginMessage = `${user.username} logged in`;
const startedWorkspaceMessage = `${user.username} started workspace ${workspaceName}`;
const loginMessage = `${username} logged in`;
const startedWorkspaceMessage = `${username} started workspace ${workspaceName}`;
// Filter by resource type
await resetSearch(page);
await resetSearch(page, username);
await page.getByText("All resource types").click();
const workspaceBuildsOption = page.getByText("Workspace Build");
await workspaceBuildsOption.scrollIntoViewIfNeeded({ timeout: 5000 });
@ -107,7 +109,7 @@ test("inspecting and filtering audit logs", async ({ page }) => {
await expect(page.getByText("All resource types")).toBeVisible();
// Filter by action type
await resetSearch(page);
await resetSearch(page, username);
await page.getByText("All actions").click();
await page.getByText("Login", { exact: true }).click();
// Logins should be visible

View File

@ -16,7 +16,7 @@ test("experiments", async ({ page }) => {
const availableExperiments = await API.getAvailableExperiments();
// Verify if the site lists the same experiments
await page.goto("/deployment/general", { waitUntil: "networkidle" });
await page.goto("/deployment/overview", { waitUntil: "domcontentloaded" });
const experimentsLocator = page.locator(
"div.options-table tr.option-experiments ul.option-array",

View File

@ -82,8 +82,8 @@ test.describe("roles admin settings access", () => {
]);
});
test("admin can see admin settings", async ({ page }) => {
await login(page, users.admin);
test("owner can see admin settings", async ({ page }) => {
await login(page, users.owner);
await page.goto("/", { waitUntil: "domcontentloaded" });
await hasAccessToAdminSettings(page, [

View File

@ -6,7 +6,7 @@ import type {
SerpentOption,
User,
} from "api/typesGenerated";
import type { Permissions } from "contexts/auth/permissions";
import type { Permissions } from "modules/permissions";
import type { QueryKey } from "react-query";
declare module "@storybook/react" {

View File

@ -9,7 +9,7 @@ import {
type OrganizationPermissionName,
type OrganizationPermissions,
organizationPermissionChecks,
} from "modules/management/organizationPermissions";
} from "modules/permissions/organizations";
import type { QueryClient } from "react-query";
import { meKey } from "./users";

View File

@ -10,6 +10,7 @@ import {
import type { UpdateUserProfileRequest, User } from "api/typesGenerated";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { type Permissions, permissionChecks } from "modules/permissions";
import {
type FC,
type PropsWithChildren,
@ -18,7 +19,6 @@ import {
useContext,
} from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { type Permissions, permissionChecks } from "./permissions";
export type AuthContextValue = {
isLoading: boolean;

View File

@ -16,8 +16,8 @@ import { useUpdateCheck } from "./useUpdateCheck";
export const DashboardLayout: FC = () => {
const { permissions } = useAuthenticated();
const updateCheck = useUpdateCheck(permissions.viewUpdateCheck);
const canViewDeployment = Boolean(permissions.viewDeploymentValues);
const updateCheck = useUpdateCheck(permissions.viewDeploymentConfig);
const canViewDeployment = Boolean(permissions.viewDeploymentConfig);
return (
<>

View File

@ -11,8 +11,8 @@ import type {
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Loader } from "components/Loader/Loader";
import { useAuthenticated } from "contexts/auth/RequireAuth";
import { canViewAnyOrganization } from "contexts/auth/permissions";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { canViewAnyOrganization } from "modules/permissions";
import { type FC, type PropsWithChildren, createContext } from "react";
import { useQuery } from "react-query";
import { selectFeatureVisibility } from "./entitlements";

View File

@ -10,10 +10,10 @@ export const DeploymentBanner: FC = () => {
const deploymentStatsQuery = useQuery(deploymentStats());
const healthQuery = useQuery({
...health(),
enabled: permissions.viewDeploymentValues,
enabled: permissions.viewDeploymentConfig,
});
if (!permissions.viewDeploymentValues || !deploymentStatsQuery.data) {
if (!permissions.viewDeploymentConfig || !deploymentStatsQuery.data) {
return null;
}

View File

@ -1,9 +1,9 @@
import { buildInfo } from "api/queries/buildInfo";
import { useProxy } from "contexts/ProxyContext";
import { useAuthenticated } from "contexts/auth/RequireAuth";
import { canViewDeploymentSettings } from "contexts/auth/permissions";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { useDashboard } from "modules/dashboard/useDashboard";
import { canViewDeploymentSettings } from "modules/permissions";
import type { FC } from "react";
import { useQuery } from "react-query";
import { useFeatureVisibility } from "../useFeatureVisibility";

View File

@ -3,7 +3,7 @@ import { fn, userEvent, within } from "@storybook/test";
import { getAuthorizationKey } from "api/queries/authCheck";
import { getPreferredProxy } from "contexts/ProxyContext";
import { AuthProvider } from "contexts/auth/AuthProvider";
import { permissionChecks } from "contexts/auth/permissions";
import { permissionChecks } from "modules/permissions";
import {
MockAuthMethodsAll,
MockPermissions,

View File

@ -6,26 +6,26 @@ import { type FC, createContext, useContext } from "react";
import { useQuery } from "react-query";
import { Outlet } from "react-router-dom";
export const DeploymentSettingsContext = createContext<
DeploymentSettingsValue | undefined
export const DeploymentConfigContext = createContext<
DeploymentConfigValue | undefined
>(undefined);
type DeploymentSettingsValue = Readonly<{
type DeploymentConfigValue = Readonly<{
deploymentConfig: DeploymentConfig;
}>;
export const useDeploymentSettings = (): DeploymentSettingsValue => {
const context = useContext(DeploymentSettingsContext);
export const useDeploymentConfig = (): DeploymentConfigValue => {
const context = useContext(DeploymentConfigContext);
if (!context) {
throw new Error(
`${useDeploymentSettings.name} should be used inside of ${DeploymentSettingsProvider.name}`,
`${useDeploymentConfig.name} should be used inside of ${DeploymentConfigProvider.name}`,
);
}
return context;
};
const DeploymentSettingsProvider: FC = () => {
const DeploymentConfigProvider: FC = () => {
const deploymentConfigQuery = useQuery(deploymentConfig());
if (deploymentConfigQuery.error) {
@ -37,12 +37,12 @@ const DeploymentSettingsProvider: FC = () => {
}
return (
<DeploymentSettingsContext.Provider
<DeploymentConfigContext.Provider
value={{ deploymentConfig: deploymentConfigQuery.data }}
>
<Outlet />
</DeploymentSettingsContext.Provider>
</DeploymentConfigContext.Provider>
);
};
export default DeploymentSettingsProvider;
export default DeploymentConfigProvider;

View File

@ -7,8 +7,8 @@ import {
} from "components/Breadcrumb/Breadcrumb";
import { Loader } from "components/Loader/Loader";
import { useAuthenticated } from "contexts/auth/RequireAuth";
import { RequirePermission } from "contexts/auth/RequirePermission";
import { canViewDeploymentSettings } from "contexts/auth/permissions";
import { canViewDeploymentSettings } from "modules/permissions";
import { RequirePermission } from "modules/permissions/RequirePermission";
import { type FC, Suspense } from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { DeploymentSidebar } from "./DeploymentSidebar";
@ -21,8 +21,8 @@ const DeploymentSettingsLayout: FC = () => {
return (
<Navigate
to={
permissions.viewDeploymentValues
? "/deployment/general"
permissions.viewDeploymentConfig
? "/deployment/overview"
: "/deployment/users"
}
replace

View File

@ -47,8 +47,8 @@ export const NoDeploymentValues: Story = {
args: {
permissions: {
...MockPermissions,
viewDeploymentValues: false,
editDeploymentValues: false,
viewDeploymentConfig: false,
editDeploymentConfig: false,
},
},
};

View File

@ -4,8 +4,8 @@ import {
SettingsSidebarNavItem as SidebarNavItem,
} from "components/Sidebar/Sidebar";
import { Stack } from "components/Stack/Stack";
import type { Permissions } from "contexts/auth/permissions";
import { ArrowUpRight } from "lucide-react";
import type { Permissions } from "modules/permissions";
import type { FC } from "react";
interface DeploymentSidebarViewProps {
@ -18,9 +18,6 @@ interface DeploymentSidebarViewProps {
/**
* Displays navigation for deployment settings. If active, highlight the main
* menu heading.
*
* Menu items are shown based on the permissions. If organizations can be
* viewed, groups are skipped since they will show under each org instead.
*/
export const DeploymentSidebarView: FC<DeploymentSidebarViewProps> = ({
permissions,
@ -30,32 +27,32 @@ export const DeploymentSidebarView: FC<DeploymentSidebarViewProps> = ({
return (
<BaseSidebar>
<div className="flex flex-col gap-1">
{permissions.viewDeploymentValues && (
<SidebarNavItem href="/deployment/general">General</SidebarNavItem>
{permissions.viewDeploymentConfig && (
<SidebarNavItem href="/deployment/overview">Overview</SidebarNavItem>
)}
{permissions.viewAllLicenses && (
<SidebarNavItem href="/deployment/licenses">Licenses</SidebarNavItem>
)}
{permissions.editDeploymentValues && (
{permissions.editDeploymentConfig && (
<SidebarNavItem href="/deployment/appearance">
Appearance
</SidebarNavItem>
)}
{permissions.viewDeploymentValues && (
{permissions.viewDeploymentConfig && (
<SidebarNavItem href="/deployment/userauth">
User Authentication
</SidebarNavItem>
)}
{permissions.viewDeploymentValues && (
{permissions.viewDeploymentConfig && (
<SidebarNavItem href="/deployment/external-auth">
External Authentication
</SidebarNavItem>
)}
{/* Not exposing this yet since token exchange is not finished yet.
<SidebarNavItem href="oauth2-provider/ap">
<SidebarNavItem href="oauth2-provider/apps">
OAuth2 Applications
</SidebarNavItem>*/}
{permissions.viewDeploymentValues && (
{permissions.viewDeploymentConfig && (
<SidebarNavItem href="/deployment/network">Network</SidebarNavItem>
)}
{permissions.readWorkspaceProxies && (
@ -63,10 +60,10 @@ export const DeploymentSidebarView: FC<DeploymentSidebarViewProps> = ({
Workspace Proxies
</SidebarNavItem>
)}
{permissions.viewDeploymentValues && (
{permissions.viewDeploymentConfig && (
<SidebarNavItem href="/deployment/security">Security</SidebarNavItem>
)}
{permissions.viewDeploymentValues && (
{permissions.viewDeploymentConfig && (
<SidebarNavItem href="/deployment/observability">
Observability
</SidebarNavItem>
@ -81,6 +78,11 @@ export const DeploymentSidebarView: FC<DeploymentSidebarViewProps> = ({
</Stack>
</SidebarNavItem>
)}
{permissions.viewOrganizationIDPSyncSettings && (
<SidebarNavItem href="/deployment/idp-org-sync">
IdP Organization Sync
</SidebarNavItem>
)}
{permissions.viewNotificationTemplate && (
<SidebarNavItem href="/deployment/notifications">
<div className="flex flex-row items-center gap-2">
@ -89,11 +91,6 @@ export const DeploymentSidebarView: FC<DeploymentSidebarViewProps> = ({
</div>
</SidebarNavItem>
)}
{permissions.viewOrganizationIDPSyncSettings && (
<SidebarNavItem href="/deployment/idp-org-sync">
IdP Organization Sync
</SidebarNavItem>
)}
{!hasPremiumLicense && (
<SidebarNavItem href="/deployment/premium">Premium</SidebarNavItem>
)}

View File

@ -11,14 +11,14 @@ import {
} from "components/Breadcrumb/Breadcrumb";
import { Loader } from "components/Loader/Loader";
import { useDashboard } from "modules/dashboard/useDashboard";
import {
type OrganizationPermissions,
canViewOrganization,
} from "modules/permissions/organizations";
import NotFoundPage from "pages/404Page/404Page";
import { type FC, Suspense, createContext, useContext } from "react";
import { useQuery } from "react-query";
import { Outlet, useParams } from "react-router-dom";
import {
type OrganizationPermissions,
canViewOrganization,
} from "./organizationPermissions";
export const OrganizationSettingsContext = createContext<
OrganizationSettingsValue | undefined
@ -46,7 +46,7 @@ export const useOrganizationSettings = (): OrganizationSettingsValue => {
};
const OrganizationSettingsLayout: FC = () => {
const { organizations, showOrganizations } = useDashboard();
const { organizations } = useDashboard();
const { organization: orgName } = useParams() as {
organization?: string;
};

View File

@ -16,11 +16,11 @@ import {
PopoverTrigger,
} from "components/Popover/Popover";
import { SettingsSidebarNavItem } from "components/Sidebar/Sidebar";
import type { Permissions } from "contexts/auth/permissions";
import { Check, ChevronDown, Plus } from "lucide-react";
import type { Permissions } from "modules/permissions";
import type { OrganizationPermissions } from "modules/permissions/organizations";
import { type FC, useState } from "react";
import { useNavigate } from "react-router-dom";
import type { OrganizationPermissions } from "./organizationPermissions";
interface OrganizationsSettingsNavigationProps {
/** The organization selected from the dropdown */

View File

@ -30,7 +30,7 @@ export const permissionChecks = {
resource_type: "template",
any_org: true,
},
action: "update",
action: "create",
},
updateTemplates: {
object: {
@ -44,30 +44,18 @@ export const permissionChecks = {
},
action: "delete",
},
viewDeploymentValues: {
viewDeploymentConfig: {
object: {
resource_type: "deployment_config",
},
action: "read",
},
editDeploymentValues: {
editDeploymentConfig: {
object: {
resource_type: "deployment_config",
},
action: "update",
},
viewUpdateCheck: {
object: {
resource_type: "deployment_config",
},
action: "read",
},
viewExternalAuthConfig: {
object: {
resource_type: "deployment_config",
},
action: "read",
},
viewDeploymentStats: {
object: {
resource_type: "deployment_stats",
@ -178,7 +166,7 @@ export const canViewDeploymentSettings = (
): permissions is Permissions => {
return (
permissions !== undefined &&
(permissions.viewDeploymentValues ||
(permissions.viewDeploymentConfig ||
permissions.viewAllLicenses ||
permissions.viewAllUsers ||
permissions.viewAnyGroup ||

View File

@ -1,11 +1,11 @@
import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider";
import { useDeploymentConfig } from "modules/management/DeploymentConfigProvider";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
import { ExternalAuthSettingsPageView } from "./ExternalAuthSettingsPageView";
const ExternalAuthSettingsPage: FC = () => {
const { deploymentConfig } = useDeploymentSettings();
const { deploymentConfig } = useDeploymentConfig();
return (
<>

View File

@ -108,7 +108,7 @@ export const LicenseSeatConsumptionChart: FC<
</li>
<li>
<Link asChild>
<RouterLink to="/deployment/general">
<RouterLink to="/deployment/overview">
Daily user activity
</RouterLink>
</Link>

View File

@ -1,11 +1,11 @@
import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider";
import { useDeploymentConfig } from "modules/management/DeploymentConfigProvider";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
import { NetworkSettingsPageView } from "./NetworkSettingsPageView";
const NetworkSettingsPage: FC = () => {
const { deploymentConfig } = useDeploymentSettings();
const { deploymentConfig } = useDeploymentConfig();
return (
<>

View File

@ -9,7 +9,7 @@ import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs";
import { useSearchParamsKey } from "hooks/useSearchParamsKey";
import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider";
import { useDeploymentConfig } from "modules/management/DeploymentConfigProvider";
import { castNotificationMethod } from "modules/notifications/utils";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
@ -22,7 +22,7 @@ import { NotificationEvents } from "./NotificationEvents";
import { Troubleshooting } from "./Troubleshooting";
export const NotificationsPage: FC = () => {
const { deploymentConfig } = useDeploymentSettings();
const { deploymentConfig } = useDeploymentConfig();
const [templatesByGroup, dispatchMethods] = useQueries({
queries: [
{

View File

@ -194,7 +194,7 @@ export const baseMeta = {
},
],
user: MockUser,
permissions: { viewDeploymentValues: true },
permissions: { viewDeploymentConfig: true },
deploymentOptions: mockNotificationsDeploymentOptions,
deploymentValues: {
notifications: {

View File

@ -1,13 +1,13 @@
import { useDashboard } from "modules/dashboard/useDashboard";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider";
import { useDeploymentConfig } from "modules/management/DeploymentConfigProvider";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
import { ObservabilitySettingsPageView } from "./ObservabilitySettingsPageView";
const ObservabilitySettingsPage: FC = () => {
const { deploymentConfig } = useDeploymentSettings();
const { deploymentConfig } = useDeploymentConfig();
const { entitlements } = useDashboard();
const { multiple_organizations: hasPremiumLicense } = useFeatureVisibility();

View File

@ -1,15 +1,15 @@
import { deploymentDAUs } from "api/queries/deployment";
import { availableExperiments, experiments } from "api/queries/experiments";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider";
import { useDeploymentConfig } from "modules/management/DeploymentConfigProvider";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useQuery } from "react-query";
import { pageTitle } from "utils/page";
import { GeneralSettingsPageView } from "./GeneralSettingsPageView";
import { OverviewPageView } from "./OverviewPageView";
const GeneralSettingsPage: FC = () => {
const { deploymentConfig } = useDeploymentSettings();
const OverviewPage: FC = () => {
const { deploymentConfig } = useDeploymentConfig();
const safeExperimentsQuery = useQuery(availableExperiments());
const { metadata } = useEmbeddedMetadata();
@ -26,9 +26,9 @@ const GeneralSettingsPage: FC = () => {
return (
<>
<Helmet>
<title>{pageTitle("General Settings")}</title>
<title>{pageTitle("Overview", "Deployment")}</title>
</Helmet>
<GeneralSettingsPageView
<OverviewPageView
deploymentOptions={deploymentConfig.options}
dailyActiveUsers={dailyActiveUsers}
invalidExperiments={invalidExperiments}
@ -38,4 +38,4 @@ const GeneralSettingsPage: FC = () => {
);
};
export default GeneralSettingsPage;
export default OverviewPage;

View File

@ -1,10 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MockDeploymentDAUResponse } from "testHelpers/entities";
import { GeneralSettingsPageView } from "./GeneralSettingsPageView";
import { OverviewPageView } from "./OverviewPageView";
const meta: Meta<typeof GeneralSettingsPageView> = {
title: "pages/DeploymentSettingsPage/GeneralSettingsPageView",
component: GeneralSettingsPageView,
const meta: Meta<typeof OverviewPageView> = {
title: "pages/DeploymentSettingsPage/OverviewPageView",
component: OverviewPageView,
args: {
deploymentOptions: [
{
@ -42,7 +42,7 @@ const meta: Meta<typeof GeneralSettingsPageView> = {
};
export default meta;
type Story = StoryObj<typeof GeneralSettingsPageView>;
type Story = StoryObj<typeof OverviewPageView>;
export const Page: Story = {};

View File

@ -14,14 +14,14 @@ import { Alert } from "../../../components/Alert/Alert";
import OptionsTable from "../OptionsTable";
import { UserEngagementChart } from "./UserEngagementChart";
export type GeneralSettingsPageViewProps = {
export type OverviewPageViewProps = {
deploymentOptions: SerpentOption[];
dailyActiveUsers: DAUsResponse | undefined;
readonly invalidExperiments: Experiments | string[];
readonly safeExperiments: Experiments | string[];
};
export const GeneralSettingsPageView: FC<GeneralSettingsPageViewProps> = ({
export const OverviewPageView: FC<OverviewPageViewProps> = ({
deploymentOptions,
dailyActiveUsers,
safeExperiments,

View File

@ -1,12 +1,12 @@
import { useDashboard } from "modules/dashboard/useDashboard";
import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider";
import { useDeploymentConfig } from "modules/management/DeploymentConfigProvider";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
import { SecuritySettingsPageView } from "./SecuritySettingsPageView";
const SecuritySettingsPage: FC = () => {
const { deploymentConfig } = useDeploymentSettings();
const { deploymentConfig } = useDeploymentConfig();
const { entitlements } = useDashboard();
return (

View File

@ -1,11 +1,11 @@
import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider";
import { useDeploymentConfig } from "modules/management/DeploymentConfigProvider";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
import { UserAuthSettingsPageView } from "./UserAuthSettingsPageView";
const UserAuthSettingsPage: FC = () => {
const { deploymentConfig } = useDeploymentSettings();
const { deploymentConfig } = useDeploymentConfig();
return (
<>

View File

@ -104,7 +104,7 @@ const ExternalAuthPage: FC = () => {
authenticated: false,
});
}}
viewExternalAuthConfig={permissions.viewExternalAuthConfig}
viewExternalAuthConfig={permissions.viewDeploymentConfig}
deviceExchangeError={deviceExchangeError}
externalAuthDevice={externalAuthDeviceQuery.data}
/>

View File

@ -1,8 +1,8 @@
import { createOrganization } from "api/queries/organizations";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { useAuthenticated } from "contexts/auth/RequireAuth";
import { RequirePermission } from "contexts/auth/RequirePermission";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { RequirePermission } from "modules/permissions/RequirePermission";
import type { FC } from "react";
import { useMutation, useQueryClient } from "react-query";
import { useNavigate } from "react-router-dom";

View File

@ -8,8 +8,8 @@ import type { CustomRoleRequest } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { displayError } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { RequirePermission } from "contexts/auth/RequirePermission";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import { RequirePermission } from "modules/permissions/RequirePermission";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQuery, useQueryClient } from "react-query";

View File

@ -6,9 +6,9 @@ import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { RequirePermission } from "contexts/auth/RequirePermission";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import { RequirePermission } from "modules/permissions/RequirePermission";
import { type FC, useEffect, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQuery, useQueryClient } from "react-query";

View File

@ -1,6 +1,6 @@
import { EmptyState } from "components/EmptyState/EmptyState";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
import { canEditOrganization } from "modules/management/organizationPermissions";
import { canEditOrganization } from "modules/permissions/organizations";
import type { FC } from "react";
import { Navigate } from "react-router-dom";
@ -10,19 +10,25 @@ const OrganizationRedirect: FC = () => {
organizationPermissionsByOrganizationId: organizationPermissions,
} = useOrganizationSettings();
const sortedOrganizations = [...organizations].sort(
(a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0),
);
// Redirect /organizations => /organizations/some-organization-name
// If they can edit the default org, we should redirect to the default.
// If they cannot edit the default, we should redirect to the first org that
// they can edit.
const editableOrg = [...organizations]
.sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0))
.find((org) => canEditOrganization(organizationPermissions[org.id]));
const editableOrg = sortedOrganizations.find((org) =>
canEditOrganization(organizationPermissions[org.id]),
);
if (editableOrg) {
return <Navigate to={`/organizations/${editableOrg.name}`} replace />;
}
// If they cannot edit any org, just redirect to an org they can read.
if (organizations.length > 0) {
return <Navigate to={`/organizations/${organizations[0].name}`} replace />;
if (sortedOrganizations.length > 0) {
return (
<Navigate to={`/organizations/${sortedOrganizations[0].name}`} replace />
);
}
return <EmptyState message="No organizations found" />;
};

View File

@ -4,7 +4,7 @@ import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
import type { Workspace, WorkspaceAgentLifecycle } from "api/typesGenerated";
import { AuthProvider } from "contexts/auth/AuthProvider";
import { RequireAuth } from "contexts/auth/RequireAuth";
import { permissionChecks } from "contexts/auth/permissions";
import { permissionChecks } from "modules/permissions";
import {
reactRouterOutlet,
reactRouterParameters,

View File

@ -40,7 +40,7 @@ const meta = {
},
],
user: MockUser,
permissions: { viewDeploymentValues: true },
permissions: { viewDeploymentConfig: true },
},
decorators: [withGlobalSnackbar, withAuthProvider, withDashboardProvider],
} satisfies Meta<typeof NotificationsPage>;
@ -74,7 +74,7 @@ export const ToggleNotification: Story = {
export const NonAdmin: Story = {
parameters: {
permissions: { viewDeploymentValues: false },
permissions: { viewDeploymentConfig: false },
},
};

View File

@ -48,7 +48,7 @@ export const NotificationsPage: FC = () => {
...systemNotificationTemplates(),
select: (data: NotificationTemplate[]) => {
const groups = selectTemplatesByGroup(data);
return permissions.viewDeploymentValues
return permissions.viewDeploymentConfig
? groups
: {
// Members only have access to the "Workspace Notifications" group

View File

@ -63,7 +63,7 @@ const parameters = {
permissions: {
createUser: true,
updateUsers: true,
viewDeploymentValues: true,
viewDeploymentConfig: true,
},
};

View File

@ -51,12 +51,12 @@ const UsersPage: FC<UserPageProps> = ({ defaultNewPassword }) => {
const {
createUser: canCreateUser,
updateUsers: canEditUsers,
viewDeploymentValues,
viewDeploymentConfig,
} = permissions;
const rolesQuery = useQuery(roles());
const { data: deploymentValues } = useQuery({
...deploymentConfig(),
enabled: viewDeploymentValues,
enabled: viewDeploymentConfig,
});
const usersQuery = usePaginatedQuery(paginatedUsers(searchParamsResult[0]));
@ -94,7 +94,7 @@ const UsersPage: FC<UserPageProps> = ({ defaultNewPassword }) => {
// Indicates if oidc roles are synced from the oidc idp.
// Assign 'false' if unknown.
const oidcRoleSyncEnabled =
viewDeploymentValues &&
viewDeploymentConfig &&
deploymentValues?.config.oidc?.user_role_field !== "";
const isLoading =

View File

@ -11,7 +11,7 @@ const permissions: WorkspacePermissions = {
readWorkspace: true,
updateWorkspace: true,
updateTemplate: true,
viewDeploymentValues: true,
viewDeploymentConfig: true,
};
const meta: Meta<typeof Workspace> = {

View File

@ -15,7 +15,7 @@ const defaultPermissions = {
readWorkspace: true,
updateTemplate: true,
updateWorkspace: true,
viewDeploymentValues: true,
viewDeploymentConfig: true,
};
const meta: Meta<typeof WorkspaceNotifications> = {

View File

@ -66,7 +66,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
// Debug mode
const { data: deploymentValues } = useQuery({
...deploymentConfig(),
enabled: permissions.viewDeploymentValues,
enabled: permissions.viewDeploymentConfig,
});
// Build logs

View File

@ -25,7 +25,7 @@ export const workspaceChecks = (workspace: Workspace, template: Template) =>
},
action: "update",
},
viewDeploymentValues: {
viewDeploymentConfig: {
object: {
resource_type: "deployment_config",
},

View File

@ -156,7 +156,7 @@ const useWorkspacesFilter = ({
});
const { permissions } = useAuthenticated();
const canFilterByUser = permissions.viewDeploymentValues;
const canFilterByUser = permissions.viewDeploymentConfig;
const userMenu = useUserFilterMenu({
value: filter.values.owner,
onChange: (option) =>

View File

@ -31,8 +31,8 @@ const NotFoundPage = lazy(() => import("./pages/404Page/404Page"));
const DeploymentSettingsLayout = lazy(
() => import("./modules/management/DeploymentSettingsLayout"),
);
const DeploymentSettingsProvider = lazy(
() => import("./modules/management/DeploymentSettingsProvider"),
const DeploymentConfigProvider = lazy(
() => import("./modules/management/DeploymentConfigProvider"),
);
const OrganizationSidebarLayout = lazy(
() => import("./modules/management/OrganizationSidebarLayout"),
@ -98,11 +98,8 @@ const TemplateSummaryPage = lazy(
const CreateWorkspacePage = lazy(
() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"),
);
const GeneralSettingsPage = lazy(
() =>
import(
"./pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPage"
),
const OverviewPage = lazy(
() => import("./pages/DeploymentSettingsPage/OverviewPage/OverviewPage"),
);
const SecuritySettingsPage = lazy(
() =>
@ -435,8 +432,8 @@ export const router = createBrowserRouter(
</Route>
<Route path="/deployment" element={<DeploymentSettingsLayout />}>
<Route element={<DeploymentSettingsProvider />}>
<Route path="general" element={<GeneralSettingsPage />} />
<Route element={<DeploymentConfigProvider />}>
<Route path="overview" element={<OverviewPage />} />
<Route path="security" element={<SecuritySettingsPage />} />
<Route
path="observability"

View File

@ -5,10 +5,10 @@ import {
} from "api/api";
import type { FieldError } from "api/errors";
import type * as TypesGen from "api/typesGenerated";
import type { Permissions } from "contexts/auth/permissions";
import type { ProxyLatencyReport } from "contexts/useProxyLatency";
import range from "lodash/range";
import type { OrganizationPermissions } from "modules/management/organizationPermissions";
import type { Permissions } from "modules/permissions";
import type { OrganizationPermissions } from "modules/permissions/organizations";
import type { FileTree } from "utils/filetree";
import type { TemplateVersionFiles } from "utils/templateVersion";
@ -2844,11 +2844,9 @@ export const MockPermissions: Permissions = {
viewAllUsers: true,
updateUsers: true,
viewAnyAuditLog: true,
viewDeploymentValues: true,
editDeploymentValues: true,
viewUpdateCheck: true,
viewDeploymentConfig: true,
editDeploymentConfig: true,
viewDeploymentStats: true,
viewExternalAuthConfig: true,
readWorkspaceProxies: true,
editWorkspaceProxies: true,
createOrganization: true,
@ -2873,11 +2871,9 @@ export const MockNoPermissions: Permissions = {
viewAllUsers: false,
updateUsers: false,
viewAnyAuditLog: false,
viewDeploymentValues: false,
editDeploymentValues: false,
viewUpdateCheck: false,
viewDeploymentConfig: false,
editDeploymentConfig: false,
viewDeploymentStats: false,
viewExternalAuthConfig: false,
readWorkspaceProxies: false,
editWorkspaceProxies: false,
createOrganization: false,

View File

@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import type { CreateWorkspaceBuildRequest } from "api/typesGenerated";
import { permissionChecks } from "contexts/auth/permissions";
import { permissionChecks } from "modules/permissions";
import { http, HttpResponse } from "msw";
import * as M from "./entities";
import { MockGroup, MockWorkspaceQuota } from "./entities";

View File

@ -6,10 +6,10 @@ import { hasFirstUserKey, meKey } from "api/queries/users";
import type { Entitlements } from "api/typesGenerated";
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
import { AuthProvider } from "contexts/auth/AuthProvider";
import { permissionChecks } from "contexts/auth/permissions";
import { DashboardContext } from "modules/dashboard/DashboardProvider";
import { DeploymentSettingsContext } from "modules/management/DeploymentSettingsProvider";
import { DeploymentConfigContext } from "modules/management/DeploymentConfigProvider";
import { OrganizationSettingsContext } from "modules/management/OrganizationSettingsLayout";
import { permissionChecks } from "modules/permissions";
import type { FC } from "react";
import { useQueryClient } from "react-query";
import {
@ -168,11 +168,11 @@ export const withOrganizationSettingsProvider = (Story: FC) => {
organizationPermissions: MockOrganizationPermissions,
}}
>
<DeploymentSettingsContext.Provider
<DeploymentConfigContext.Provider
value={{ deploymentConfig: MockDeploymentConfig }}
>
<Story />
</DeploymentSettingsContext.Provider>
</DeploymentConfigContext.Provider>
</OrganizationSettingsContext.Provider>
);
};