diff --git a/.vscode/settings.json b/.vscode/settings.json index 2476e330cd306..6695a12faa8dc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -175,6 +175,7 @@ "unauthenticate", "unconvert", "untar", + "userauth", "userspace", "VMID", "walkthrough", diff --git a/site/e2e/global.setup.ts b/site/e2e/global.setup.ts index 6eafd2886de37..f39a2d475804e 100644 --- a/site/e2e/global.setup.ts +++ b/site/e2e/global.setup.ts @@ -35,6 +35,7 @@ test("setup deployment", async ({ page }) => { expect(constants.license.split(".").length).toBe(3); // otherwise it's invalid await page.goto("/deployment/licenses", { waitUntil: "domcontentloaded" }); + await expect(page).toHaveTitle("License Settings - Coder"); await page.getByText("Add a license").click(); await page.getByRole("textbox").fill(constants.license); diff --git a/site/jest.setup.ts b/site/jest.setup.ts index 7a06ebba2592f..7d4b6f0772bc4 100644 --- a/site/jest.setup.ts +++ b/site/jest.setup.ts @@ -1,7 +1,7 @@ import "@testing-library/jest-dom"; import "jest-location-mock"; import { cleanup } from "@testing-library/react"; -import crypto from "crypto"; +import crypto from "node:crypto"; import { useMemo } from "react"; import type { Region } from "api/typesGenerated"; import type { ProxyLatencyReport } from "contexts/useProxyLatency"; @@ -48,9 +48,7 @@ global.ResizeObserver = require("resize-observer-polyfill"); // Polyfill the getRandomValues that is used on utils/random.ts Object.defineProperty(global.self, "crypto", { value: { - getRandomValues: function (buffer: Buffer) { - return crypto.randomFillSync(buffer); - }, + getRandomValues: crypto.randomFillSync, }, }); @@ -72,5 +70,5 @@ afterEach(() => { // Clean up after the tests are finished. afterAll(() => server.close()); -// This is needed because we are compiling under `--isolatedModules` +// biome-ignore lint/complexity/noUselessEmptyExport: This is needed because we are compiling under `--isolatedModules` export {}; diff --git a/site/src/contexts/auth/permissions.tsx b/site/src/contexts/auth/permissions.tsx index 0c89d81686d2f..d2de7864874f0 100644 --- a/site/src/contexts/auth/permissions.tsx +++ b/site/src/contexts/auth/permissions.tsx @@ -1,3 +1,5 @@ +import type { AuthorizationCheck } from "api/typesGenerated"; + export const checks = { viewAllUsers: "viewAllUsers", updateUsers: "updateUsers", @@ -11,13 +13,20 @@ export const checks = { viewUpdateCheck: "viewUpdateCheck", viewExternalAuthConfig: "viewExternalAuthConfig", viewDeploymentStats: "viewDeploymentStats", + readWorkspaceProxies: "readWorkspaceProxies", editWorkspaceProxies: "editWorkspaceProxies", createOrganization: "createOrganization", editAnyOrganization: "editAnyOrganization", viewAnyGroup: "viewAnyGroup", createGroup: "createGroup", viewAllLicenses: "viewAllLicenses", -} as const; + viewNotificationTemplate: "viewNotificationTemplate", +} as const satisfies Record; + +// Type expression seems a little redundant (`keyof typeof checks` has the same +// result), just because each key-value pair is currently symmetrical; this may +// change down the line +type PermissionValue = (typeof checks)[keyof typeof checks]; export const permissionsToCheck = { [checks.viewAllUsers]: { @@ -94,6 +103,12 @@ export const permissionsToCheck = { }, action: "read", }, + [checks.readWorkspaceProxies]: { + object: { + resource_type: "workspace_proxy", + }, + action: "read", + }, [checks.editWorkspaceProxies]: { object: { resource_type: "workspace_proxy", @@ -116,7 +131,6 @@ export const permissionsToCheck = { [checks.viewAnyGroup]: { object: { resource_type: "group", - org_id: "any", }, action: "read", }, @@ -132,6 +146,12 @@ export const permissionsToCheck = { }, action: "read", }, -} as const; + [checks.viewNotificationTemplate]: { + object: { + resource_type: "notification_template", + }, + action: "read", + }, +} as const satisfies Record; -export type Permissions = Record; +export type Permissions = Record; diff --git a/site/src/modules/management/DeploymentSettingsProvider.tsx b/site/src/modules/management/DeploymentSettingsProvider.tsx new file mode 100644 index 0000000000000..c9f6cd5f4a8ce --- /dev/null +++ b/site/src/modules/management/DeploymentSettingsProvider.tsx @@ -0,0 +1,64 @@ +import type { DeploymentConfig } from "api/api"; +import { deploymentConfig } from "api/queries/deployment"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Loader } from "components/Loader/Loader"; +import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { RequirePermission } from "contexts/auth/RequirePermission"; +import { type FC, createContext, useContext } from "react"; +import { useQuery } from "react-query"; +import { Outlet } from "react-router-dom"; + +export const DeploymentSettingsContext = createContext< + DeploymentSettingsValue | undefined +>(undefined); + +type DeploymentSettingsValue = Readonly<{ + deploymentConfig: DeploymentConfig; +}>; + +export const useDeploymentSettings = (): DeploymentSettingsValue => { + const context = useContext(DeploymentSettingsContext); + if (!context) { + throw new Error( + `${useDeploymentSettings.name} should be used inside of ${DeploymentSettingsProvider.name}`, + ); + } + + return context; +}; + +const DeploymentSettingsProvider: FC = () => { + const { permissions } = useAuthenticated(); + const deploymentConfigQuery = useQuery(deploymentConfig()); + + // The deployment settings page also contains users, audit logs, groups and + // organizations, so this page must be visible if you can see any of these. + const canViewDeploymentSettingsPage = + permissions.viewDeploymentValues || + permissions.viewAllUsers || + permissions.editAnyOrganization || + permissions.viewAnyAuditLog; + + // Not a huge problem to unload the content in the event of an error, + // because the sidebar rendering isn't tied to this. Even if the user hits + // a 403 error, they'll still have navigation options + if (deploymentConfigQuery.error) { + return ; + } + + if (!deploymentConfigQuery.data) { + return ; + } + + return ( + + + + + + ); +}; + +export default DeploymentSettingsProvider; diff --git a/site/src/modules/management/ManagementSettingsLayout.tsx b/site/src/modules/management/ManagementSettingsLayout.tsx index b9fcbc0936e4b..0cb313f0e53b9 100644 --- a/site/src/modules/management/ManagementSettingsLayout.tsx +++ b/site/src/modules/management/ManagementSettingsLayout.tsx @@ -1,7 +1,4 @@ -import type { DeploymentConfig } from "api/api"; -import { deploymentConfig } from "api/queries/deployment"; import type { AuthorizationResponse, Organization } from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; import { Stack } from "components/Stack/Stack"; @@ -9,7 +6,6 @@ import { useAuthenticated } from "contexts/auth/RequireAuth"; import { RequirePermission } from "contexts/auth/RequirePermission"; import { useDashboard } from "modules/dashboard/useDashboard"; import { type FC, Suspense, createContext, useContext } from "react"; -import { useQuery } from "react-query"; import { Outlet, useParams } from "react-router-dom"; import { Sidebar } from "./Sidebar"; @@ -18,7 +14,6 @@ export const ManagementSettingsContext = createContext< >(undefined); type ManagementSettingsValue = Readonly<{ - deploymentValues: DeploymentConfig; organizations: readonly Organization[]; organization?: Organization; }>; @@ -48,15 +43,8 @@ export const canEditOrganization = ( ); }; -/** - * A multi-org capable settings page layout. - * - * If multi-org is not enabled or licensed, this is the wrong layout to use. - * See DeploySettingsLayoutInner instead. - */ -export const ManagementSettingsLayout: FC = () => { +const ManagementSettingsLayout: FC = () => { const { permissions } = useAuthenticated(); - const deploymentConfigQuery = useQuery(deploymentConfig()); const { organizations } = useDashboard(); const { organization: orgName } = useParams() as { organization?: string; @@ -70,14 +58,6 @@ export const ManagementSettingsLayout: FC = () => { permissions.editAnyOrganization || permissions.viewAnyAuditLog; - if (deploymentConfigQuery.error) { - return ; - } - - if (!deploymentConfigQuery.data) { - return ; - } - const organization = organizations && orgName ? organizations.find((org) => org.name === orgName) @@ -87,7 +67,6 @@ export const ManagementSettingsLayout: FC = () => { { -
+
}> @@ -106,3 +85,5 @@ export const ManagementSettingsLayout: FC = () => { ); }; + +export default ManagementSettingsLayout; diff --git a/site/src/modules/management/SidebarView.stories.tsx b/site/src/modules/management/SidebarView.stories.tsx index 2ddcf7750bc8d..6ffe4480261c9 100644 --- a/site/src/modules/management/SidebarView.stories.tsx +++ b/site/src/modules/management/SidebarView.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { + MockNoPermissions, MockOrganization, MockOrganization2, MockPermissions, @@ -96,7 +97,7 @@ export const NoDeploymentValues: Story = { export const NoPermissions: Story = { args: { - permissions: {}, + permissions: MockNoPermissions, }, }; diff --git a/site/src/modules/management/SidebarView.tsx b/site/src/modules/management/SidebarView.tsx index b4099a4dd7815..e6c99769e529f 100644 --- a/site/src/modules/management/SidebarView.tsx +++ b/site/src/modules/management/SidebarView.tsx @@ -2,19 +2,15 @@ import { cx } from "@emotion/css"; import type { Interpolation, Theme } from "@emotion/react"; import AddIcon from "@mui/icons-material/Add"; import SettingsIcon from "@mui/icons-material/Settings"; -import type { - AuthorizationResponse, - Experiments, - Organization, -} from "api/typesGenerated"; +import type { AuthorizationResponse, Organization } from "api/typesGenerated"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Loader } from "components/Loader/Loader"; import { Sidebar as BaseSidebar } from "components/Sidebar/Sidebar"; import { Stack } from "components/Stack/Stack"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; +import type { Permissions } from "contexts/auth/permissions"; import { type ClassName, useClassName } from "hooks/useClassName"; import { useDashboard } from "modules/dashboard/useDashboard"; -import { linkToUsers } from "modules/navigation"; import type { FC, ReactNode } from "react"; import { Link, NavLink } from "react-router-dom"; @@ -30,7 +26,7 @@ interface SidebarProps { /** Organizations and their permissions or undefined if still fetching. */ organizations: OrganizationWithPermissions[] | undefined; /** Site-wide permissions. */ - permissions: AuthorizationResponse; + permissions: Permissions; } /** @@ -72,7 +68,7 @@ interface DeploymentSettingsNavigationProps { /** Whether a deployment setting page is being viewed. */ active: boolean; /** Site-wide permissions. */ - permissions: AuthorizationResponse; + permissions: Permissions; } /** @@ -130,10 +126,11 @@ const DeploymentSettingsNavigation: FC = ({ {permissions.viewDeploymentValues && ( Network )} - {/* All users can view workspace regions. */} - - Workspace Proxies - + {permissions.readWorkspaceProxies && ( + + Workspace Proxies + + )} {permissions.viewDeploymentValues && ( Security )} @@ -145,12 +142,14 @@ const DeploymentSettingsNavigation: FC = ({ {permissions.viewAllUsers && ( Users )} - - - Notifications - - - + {permissions.viewNotificationTemplate && ( + + + Notifications + + + + )} )} @@ -167,7 +166,7 @@ interface OrganizationsSettingsNavigationProps { /** Organizations and their permissions or undefined if still fetching. */ organizations: OrganizationWithPermissions[] | undefined; /** Site-wide permissions. */ - permissions: AuthorizationResponse; + permissions: Permissions; } /** @@ -241,8 +240,6 @@ interface OrganizationSettingsNavigationProps { const OrganizationSettingsNavigation: FC< OrganizationSettingsNavigationProps > = ({ active, organization }) => { - const { experiments } = useDashboard(); - return ( <> { - const { deploymentValues } = useManagementSettings(); + const { deploymentConfig } = useDeploymentSettings(); return ( <> {pageTitle("External Authentication Settings")} - - {deploymentValues ? ( - - ) : ( - - )} + ); }; diff --git a/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx b/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx index 5d3879e195996..2b094cbf89b26 100644 --- a/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx @@ -1,9 +1,8 @@ import { deploymentDAUs } from "api/queries/deployment"; import { entitlements } from "api/queries/entitlements"; import { availableExperiments, experiments } from "api/queries/experiments"; -import { Loader } from "components/Loader/Loader"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; -import { useManagementSettings } from "modules/management/ManagementSettingsLayout"; +import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; @@ -11,7 +10,7 @@ import { pageTitle } from "utils/page"; import { GeneralSettingsPageView } from "./GeneralSettingsPageView"; const GeneralSettingsPage: FC = () => { - const { deploymentValues } = useManagementSettings(); + const { deploymentConfig } = useDeploymentSettings(); const deploymentDAUsQuery = useQuery(deploymentDAUs()); const safeExperimentsQuery = useQuery(availableExperiments()); @@ -30,18 +29,14 @@ const GeneralSettingsPage: FC = () => { {pageTitle("General Settings")} - {deploymentValues ? ( - - ) : ( - - )} + ); }; diff --git a/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPage.tsx b/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPage.tsx index 6ebd005f71031..ec77bb95e5241 100644 --- a/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/NetworkSettingsPage/NetworkSettingsPage.tsx @@ -1,24 +1,19 @@ import { Loader } from "components/Loader/Loader"; -import { useManagementSettings } from "modules/management/ManagementSettingsLayout"; +import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; import { NetworkSettingsPageView } from "./NetworkSettingsPageView"; const NetworkSettingsPage: FC = () => { - const { deploymentValues } = useManagementSettings(); + const { deploymentConfig } = useDeploymentSettings(); return ( <> {pageTitle("Network Settings")} - - {deploymentValues ? ( - - ) : ( - - )} + ); }; diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx index c2e8479a26f8c..61a1eddcd1a78 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx @@ -14,7 +14,7 @@ const meta: Meta = { defaultMethod: "smtp", availableMethods: ["smtp", "webhook"], templatesByGroup: selectTemplatesByGroup(MockNotificationTemplates), - deploymentValues: baseMeta.parameters.deploymentValues, + deploymentConfig: baseMeta.parameters.deploymentValues, }, ...baseMeta, }; @@ -25,7 +25,7 @@ type Story = StoryObj; export const SMTPNotConfigured: Story = { args: { - deploymentValues: { + deploymentConfig: { notifications: { webhook: { endpoint: "https://example.com", @@ -40,7 +40,7 @@ export const SMTPNotConfigured: Story = { export const WebhookNotConfigured: Story = { args: { - deploymentValues: { + deploymentConfig: { notifications: { webhook: { endpoint: "", diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx index 191e2eda6958e..38c36fc52c044 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx @@ -31,20 +31,20 @@ type NotificationEventsProps = { defaultMethod: NotificationMethod; availableMethods: NotificationMethod[]; templatesByGroup: ReturnType; - deploymentValues: DeploymentValues; + deploymentConfig: DeploymentValues; }; export const NotificationEvents: FC = ({ defaultMethod, availableMethods, templatesByGroup, - deploymentValues, + deploymentConfig, }) => { // Webhook const hasWebhookNotifications = Object.values(templatesByGroup) .flat() .some((t) => t.method === "webhook"); - const webhookValues = deploymentValues.notifications?.webhook ?? {}; + const webhookValues = deploymentConfig.notifications?.webhook ?? {}; const isWebhookConfigured = requiredFieldsArePresent(webhookValues, [ "endpoint", ]); @@ -53,7 +53,7 @@ export const NotificationEvents: FC = ({ const hasSMTPNotifications = Object.values(templatesByGroup) .flat() .some((t) => t.method === "smtp"); - const smtpValues = deploymentValues.notifications?.email ?? {}; + const smtpValues = deploymentConfig.notifications?.email ?? {}; const isSMTPConfigured = requiredFieldsArePresent(smtpValues, [ "smarthost", "from", diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx index d43c8c3a841a6..23f8e6b42651e 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx @@ -6,21 +6,20 @@ import { } from "api/queries/notifications"; import { Loader } from "components/Loader/Loader"; import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; -import { useManagementSettings } from "modules/management/ManagementSettingsLayout"; +import { useSearchParamsKey } from "hooks/useSearchParamsKey"; +import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider"; import { castNotificationMethod } from "modules/notifications/utils"; import { Section } from "pages/UserSettingsPage/Section"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQueries } from "react-query"; -import { useSearchParams } from "react-router-dom"; import { deploymentGroupHasParent } from "utils/deployOptions"; import { pageTitle } from "utils/page"; import OptionsTable from "../OptionsTable"; import { NotificationEvents } from "./NotificationEvents"; export const NotificationsPage: FC = () => { - const [searchParams] = useSearchParams(); - const { deploymentValues } = useManagementSettings(); + const { deploymentConfig } = useDeploymentSettings(); const [templatesByGroup, dispatchMethods] = useQueries({ queries: [ { @@ -30,10 +29,12 @@ export const NotificationsPage: FC = () => { notificationDispatchMethods(), ], }); - const ready = - templatesByGroup.data && dispatchMethods.data && deploymentValues; - const tab = searchParams.get("tab") || "events"; + const tabState = useSearchParamsKey({ + key: "tab", + defaultValue: "events", + }); + const ready = !!(templatesByGroup.data && dispatchMethods.data); return ( <> @@ -45,7 +46,7 @@ export const NotificationsPage: FC = () => { layout="fluid" featureStage={"beta"} > - + Events @@ -58,10 +59,10 @@ export const NotificationsPage: FC = () => {
{ready ? ( - tab === "events" ? ( + tabState.value === "events" ? ( { /> ) : ( + options={deploymentConfig.options.filter((o) => deploymentGroupHasParent(o.group, "Notifications"), )} /> diff --git a/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPage.tsx b/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPage.tsx index 1ea1a2d19ef82..12b574c177384 100644 --- a/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/ObservabilitySettingsPage/ObservabilitySettingsPage.tsx @@ -1,14 +1,13 @@ -import { Loader } from "components/Loader/Loader"; import { useDashboard } from "modules/dashboard/useDashboard"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; -import { useManagementSettings } from "modules/management/ManagementSettingsLayout"; +import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; import { ObservabilitySettingsPageView } from "./ObservabilitySettingsPageView"; const ObservabilitySettingsPage: FC = () => { - const { deploymentValues } = useManagementSettings(); + const { deploymentConfig } = useDeploymentSettings(); const { entitlements } = useDashboard(); const { multiple_organizations: hasPremiumLicense } = useFeatureVisibility(); @@ -17,16 +16,11 @@ const ObservabilitySettingsPage: FC = () => { {pageTitle("Observability Settings")} - - {deploymentValues ? ( - - ) : ( - - )} + ); }; diff --git a/site/src/pages/DeploymentSettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx b/site/src/pages/DeploymentSettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx index 2a296fc9d22d2..bda0988f01966 100644 --- a/site/src/pages/DeploymentSettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx @@ -1,13 +1,13 @@ import { Loader } from "components/Loader/Loader"; import { useDashboard } from "modules/dashboard/useDashboard"; -import { useManagementSettings } from "modules/management/ManagementSettingsLayout"; +import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; import { SecuritySettingsPageView } from "./SecuritySettingsPageView"; const SecuritySettingsPage: FC = () => { - const { deploymentValues } = useManagementSettings(); + const { deploymentConfig } = useDeploymentSettings(); const { entitlements } = useDashboard(); return ( @@ -15,15 +15,10 @@ const SecuritySettingsPage: FC = () => { {pageTitle("Security Settings")} - - {deploymentValues ? ( - - ) : ( - - )} + ); }; diff --git a/site/src/pages/DeploymentSettingsPage/UserAuthSettingsPage/UserAuthSettingsPage.tsx b/site/src/pages/DeploymentSettingsPage/UserAuthSettingsPage/UserAuthSettingsPage.tsx index b6382f5a54f99..1511e29aca2d0 100644 --- a/site/src/pages/DeploymentSettingsPage/UserAuthSettingsPage/UserAuthSettingsPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/UserAuthSettingsPage/UserAuthSettingsPage.tsx @@ -1,24 +1,19 @@ import { Loader } from "components/Loader/Loader"; -import { useManagementSettings } from "modules/management/ManagementSettingsLayout"; +import { useDeploymentSettings } from "modules/management/DeploymentSettingsProvider"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; import { UserAuthSettingsPageView } from "./UserAuthSettingsPageView"; const UserAuthSettingsPage: FC = () => { - const { deploymentValues } = useManagementSettings(); + const { deploymentConfig } = useDeploymentSettings(); return ( <> {pageTitle("User Authentication Settings")} - - {deploymentValues ? ( - - ) : ( - - )} + ); }; diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage.tsx index 80995a160b67d..e770a400af2a7 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage.tsx +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage.tsx @@ -19,6 +19,7 @@ import CreateEditRolePageView from "./CreateEditRolePageView"; export const CreateEditRolePage: FC = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); + const { organization: organizationName, roleName } = useParams() as { organization: string; roleName: string; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx index bd91c348e03ee..19387a28730eb 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx @@ -14,15 +14,10 @@ const OrganizationProvisionersPage: FC = () => { const { organization: organizationName } = useParams() as { organization: string; }; - const { organizations } = useManagementSettings(); + const { organization } = useManagementSettings(); const { entitlements } = useDashboard(); - const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - - const organization = organizations - ? getOrganizationByName(organizations, organizationName) - : undefined; const provisionersQuery = useQuery(provisionerDaemonGroups(organizationName)); if (!organization) { @@ -40,8 +35,3 @@ const OrganizationProvisionersPage: FC = () => { }; export default OrganizationProvisionersPage; - -const getOrganizationByName = ( - organizations: readonly Organization[], - name: string, -) => organizations.find((org) => org.name === name); diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx index 9c85f89a62b55..f6b6b49c88d37 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.stories.tsx @@ -1,6 +1,11 @@ import type { Meta, StoryObj } from "@storybook/react"; import { reactRouterParameters } from "storybook-addon-remix-react-router"; -import { MockDefaultOrganization, MockUser } from "testHelpers/entities"; +import { + MockDefaultOrganization, + MockOrganization, + MockOrganization2, + MockUser, +} from "testHelpers/entities"; import { withAuthProvider, withDashboardProvider, diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx index 1d11c85a605ae..2b4eb18a9a524 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx @@ -35,10 +35,7 @@ const OrganizationSettingsPage: FC = () => { deleteOrganization(queryClient), ); - const organization = - organizations && organizationName - ? getOrganizationByName(organizations, organizationName) - : undefined; + const organization = organizations?.find((o) => o.name === organizationName); const permissionsQuery = useQuery( organizationsPermissions(organizations?.map((o) => o.id)), ); @@ -55,13 +52,10 @@ const OrganizationSettingsPage: FC = () => { // Redirect /organizations => /organizations/default-org, or if they cannot edit // the default org, then the first org they can edit, if any. if (!organizationName) { + // .find will stop at the first match found; make sure default + // organizations are placed first 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); - }) + .sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0)) .find((org) => canEditOrganization(permissions[org.id])); if (editableOrg) { return ; @@ -111,10 +105,3 @@ const OrganizationSettingsPage: FC = () => { }; export default OrganizationSettingsPage; - -const getOrganizationByName = ( - organizations: readonly Organization[], - name: string, -) => { - return organizations.find((org) => org.name === name); -}; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx index 9983c25080a59..3e8b1ad3133b7 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx @@ -4,7 +4,6 @@ import { MockDefaultOrganization, MockOrganization, } from "testHelpers/entities"; -import { withManagementSettingsProvider } from "testHelpers/storybook"; import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView"; const meta: Meta = { diff --git a/site/src/router.tsx b/site/src/router.tsx index 2531c823b9f48..c9d8736979c34 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -10,7 +10,6 @@ import { import { Loader } from "./components/Loader/Loader"; import { RequireAuth } from "./contexts/auth/RequireAuth"; import { DashboardLayout } from "./modules/dashboard/DashboardLayout"; -import { ManagementSettingsLayout } from "./modules/management/ManagementSettingsLayout"; import AuditPage from "./pages/AuditPage/AuditPage"; import { HealthLayout } from "./pages/HealthPage/HealthLayout"; import LoginPage from "./pages/LoginPage/LoginPage"; @@ -28,6 +27,12 @@ import WorkspacesPage from "./pages/WorkspacesPage/WorkspacesPage"; // - Pages that are secondary, not in the main navigation or not usually accessed // - Pages that use heavy dependencies like charts or time libraries const NotFoundPage = lazy(() => import("./pages/404Page/404Page")); +const ManagementSettingsLayout = lazy( + () => import("./modules/management/ManagementSettingsLayout"), +); +const DeploymentSettingsProvider = lazy( + () => import("./modules/management/DeploymentSettingsProvider"), +); const CliAuthenticationPage = lazy( () => import("./pages/CliAuthPage/CliAuthPage"), ); @@ -427,22 +432,32 @@ export const router = createBrowserRouter( }> - } /> - } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } - /> + }> + } /> + } /> + } + /> + } /> + } /> + } + /> + + } + /> + + + } /> + } /> + + } /> + } /> } /> @@ -452,14 +467,9 @@ export const router = createBrowserRouter( - } /> } /> } /> {groupsRouter()} - } - /> }> diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 0db6e80d435d6..1593790e9792d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2766,12 +2766,37 @@ export const MockPermissions: Permissions = { viewUpdateCheck: true, viewDeploymentStats: true, viewExternalAuthConfig: true, + readWorkspaceProxies: true, editWorkspaceProxies: true, createOrganization: true, editAnyOrganization: true, viewAnyGroup: true, createGroup: true, viewAllLicenses: true, + viewNotificationTemplate: true, +}; + +export const MockNoPermissions: Permissions = { + createTemplates: false, + createUser: false, + deleteTemplates: false, + updateTemplates: false, + viewAllUsers: false, + updateUsers: false, + viewAnyAuditLog: false, + viewDeploymentValues: false, + editDeploymentValues: false, + viewUpdateCheck: false, + viewDeploymentStats: false, + viewExternalAuthConfig: false, + readWorkspaceProxies: false, + editWorkspaceProxies: false, + createOrganization: false, + editAnyOrganization: false, + viewAnyGroup: false, + createGroup: false, + viewAllLicenses: false, + viewNotificationTemplate: false, }; export const MockDeploymentConfig: DeploymentConfig = { diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index f093adb1cfb4a..46ae893927801 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -9,7 +9,7 @@ import { ThemeProvider } from "contexts/ThemeProvider"; import { RequireAuth } from "contexts/auth/RequireAuth"; import { DashboardLayout } from "modules/dashboard/DashboardLayout"; import type { DashboardProvider } from "modules/dashboard/DashboardProvider"; -import { ManagementSettingsLayout } from "modules/management/ManagementSettingsLayout"; +import ManagementSettingsLayout from "modules/management/ManagementSettingsLayout"; import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"; import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"; import type { ReactNode } from "react"; diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index a76e1230205fc..e905a9b412c2c 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -7,6 +7,7 @@ import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar"; import { AuthProvider } from "contexts/auth/AuthProvider"; import { permissionsToCheck } from "contexts/auth/permissions"; import { DashboardContext } from "modules/dashboard/DashboardProvider"; +import { DeploymentSettingsContext } from "modules/management/DeploymentSettingsProvider"; import { ManagementSettingsContext } from "modules/management/ManagementSettingsLayout"; import type { FC } from "react"; import { useQueryClient } from "react-query"; @@ -131,12 +132,15 @@ export const withManagementSettingsProvider = (Story: FC) => { return ( - + + + ); }; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy