diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index e532ebcd81d43..7658abd8e5304 100644 --- a/site/src/api/queries/groups.ts +++ b/site/src/api/queries/groups.ts @@ -6,22 +6,28 @@ import type { PatchGroupRequest, } from "api/typesGenerated"; -const GROUPS_QUERY_KEY = ["groups"]; type GroupSortOrder = "asc" | "desc"; -const getGroupQueryKey = (organizationId: string, groupName: string) => [ +const getGroupsQueryKey = (organizationId: string) => [ + "organization", organizationId, - "group", - groupName, + "groups", ]; export const groups = (organizationId: string) => { return { - queryKey: GROUPS_QUERY_KEY, + queryKey: getGroupsQueryKey(organizationId), queryFn: () => API.getGroups(organizationId), } satisfies UseQueryOptions; }; +const getGroupQueryKey = (organizationId: string, groupName: string) => [ + "organization", + organizationId, + "group", + groupName, +]; + export const group = (organizationId: string, groupName: string) => { return { queryKey: getGroupQueryKey(organizationId, groupName), @@ -97,7 +103,7 @@ export const createGroup = ( mutationFn: (request: CreateGroupRequest) => API.createGroup(organizationId, request), onSuccess: async () => { - await queryClient.invalidateQueries(GROUPS_QUERY_KEY); + await queryClient.invalidateQueries(getGroupsQueryKey(organizationId)); }, }; }; @@ -146,7 +152,7 @@ export const invalidateGroup = ( groupId: string, ) => Promise.all([ - queryClient.invalidateQueries(GROUPS_QUERY_KEY), + queryClient.invalidateQueries(getGroupsQueryKey(organizationId)), queryClient.invalidateQueries(getGroupQueryKey(organizationId, groupId)), ]); diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index cf1b3b842c4e3..b480f6a20891c 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -18,6 +18,9 @@ export const Navbar: FC = () => { const canViewAuditLog = featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog); const canViewDeployment = Boolean(permissions.viewDeploymentValues); + const canViewOrganizations = + featureVisibility.multiple_organizations && + experiments.includes("multi-organization"); const canViewAllUsers = Boolean(permissions.readAllUsers); const proxyContextValue = useProxy(); const canViewHealth = canViewDeployment; @@ -30,7 +33,7 @@ export const Navbar: FC = () => { supportLinks={appearance.support_links} onSignOut={signOut} canViewDeployment={canViewDeployment} - canViewOrganizations={experiments.includes("multi-organization")} + canViewOrganizations={canViewOrganizations} canViewAllUsers={canViewAllUsers} canViewHealth={canViewHealth} canViewAuditLog={canViewAuditLog} diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx index a51d67d63ce34..310f51eda8eed 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx @@ -11,7 +11,7 @@ export const CreateGroupPage: FC = () => { const navigate = useNavigate(); const { organization } = useParams() as { organization: string }; const createGroupMutation = useMutation( - createGroup(queryClient, organization), + createGroup(queryClient, organization ?? "default"), ); return ( @@ -22,7 +22,11 @@ export const CreateGroupPage: FC = () => { { const newGroup = await createGroupMutation.mutateAsync(data); - navigate(`/organizations/${organization}/groups/${newGroup.name}`); + navigate( + organization + ? `/organizations/${organization}/groups/${newGroup.name}` + : `/deployment/groups/${newGroup.name}`, + ); }} error={createGroupMutation.error} isLoading={createGroupMutation.isLoading} diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx index 2ab2e0c2fd8b4..b3be7c472d11c 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx @@ -50,7 +50,7 @@ import { isEveryoneGroup } from "utils/groups"; import { pageTitle } from "utils/page"; export const GroupPage: FC = () => { - const { organization, groupName } = useParams() as { + const { organization = "default", groupName } = useParams() as { organization: string; groupName: string; }; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx index ca9b836c4ba5c..e07f44aeb99e6 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx @@ -11,7 +11,7 @@ import { pageTitle } from "utils/page"; import GroupSettingsPageView from "./GroupSettingsPageView"; export const GroupSettingsPage: FC = () => { - const { organization, groupName } = useParams() as { + const { organization = "default", groupName } = useParams() as { organization: string; groupName: string; }; diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx index 91d727589d8b2..d8c756645363d 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx @@ -3,12 +3,19 @@ import Button from "@mui/material/Button"; import { type FC, useEffect } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; -import { Link as RouterLink } from "react-router-dom"; +import { + Navigate, + Link as RouterLink, + useLocation, + useParams, +} from "react-router-dom"; import { getErrorMessage } from "api/errors"; import { groups } from "api/queries/groups"; +import type { Organization } from "api/typesGenerated"; import { displayError } from "components/GlobalSnackbar/utils"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { useDashboard } from "modules/dashboard/useDashboard"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { pageTitle } from "utils/page"; import { useOrganizationSettings } from "../ManagementSettingsLayout"; @@ -16,10 +23,16 @@ import GroupsPageView from "./GroupsPageView"; export const GroupsPage: FC = () => { const { permissions } = useAuthenticated(); - const { currentOrganizationId } = useOrganizationSettings(); const { createGroup: canCreateGroup } = permissions; - const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); - const groupsQuery = useQuery(groups(currentOrganizationId!)); + const { + multiple_organizations: organizationsEnabled, + template_rbac: isTemplateRBACEnabled, + } = useFeatureVisibility(); + const { experiments } = useDashboard(); + const location = useLocation(); + const { organization = "default" } = useParams() as { organization: string }; + const groupsQuery = useQuery(groups(organization)); + const { organizations } = useOrganizationSettings(); useEffect(() => { if (groupsQuery.error) { @@ -29,6 +42,16 @@ export const GroupsPage: FC = () => { } }, [groupsQuery.error]); + if ( + organizationsEnabled && + experiments.includes("multi-organization") && + location.pathname === "/deployment/groups" + ) { + const defaultName = + getOrganizationNameByDefault(organizations) ?? "default"; + return ; + } + return ( <> @@ -63,3 +86,6 @@ export const GroupsPage: FC = () => { }; export default GroupsPage; + +export const getOrganizationNameByDefault = (organizations: Organization[]) => + organizations.find((org) => org.is_default)?.name; diff --git a/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx b/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx index 026358c18a99e..6831fbc6a7db1 100644 --- a/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx +++ b/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx @@ -1,6 +1,6 @@ import { createContext, type FC, Suspense, useContext } from "react"; import { useQuery } from "react-query"; -import { Outlet, useLocation, useParams } from "react-router-dom"; +import { Outlet } from "react-router-dom"; import { deploymentConfig } from "api/queries/deployment"; import { organizations } from "api/queries/organizations"; import type { Organization } from "api/typesGenerated"; @@ -15,7 +15,6 @@ import { DeploySettingsContext } from "../DeploySettingsPage/DeploySettingsLayou import { Sidebar } from "./Sidebar"; type OrganizationSettingsContextValue = { - currentOrganizationId?: string; organizations: Organization[]; }; @@ -34,19 +33,13 @@ export const useOrganizationSettings = (): OrganizationSettingsContextValue => { }; export const ManagementSettingsLayout: FC = () => { - const location = useLocation(); const { permissions } = useAuthenticated(); const { experiments } = useDashboard(); - const { organization } = useParams() as { organization: string }; const deploymentConfigQuery = useQuery(deploymentConfig()); const organizationsQuery = useQuery(organizations()); const multiOrgExperimentEnabled = experiments.includes("multi-organization"); - const inOrganizationSettings = - location.pathname.startsWith("/organizations") && - location.pathname !== "/organizations/new"; - if (!multiOrgExperimentEnabled) { return ; } @@ -57,17 +50,7 @@ export const ManagementSettingsLayout: FC = () => { {organizationsQuery.data ? (
@@ -94,9 +77,3 @@ export const ManagementSettingsLayout: FC = () => { ); }; - -const getOrganizationIdByName = (organizations: Organization[], name: string) => - organizations.find((org) => org.name === name)?.id; - -const getOrganizationIdByDefault = (organizations: Organization[]) => - organizations.find((org) => org.is_default)?.id; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx index 19831dc8cfbf6..a8cc5362b8d57 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx @@ -1,18 +1,23 @@ import type { FC } from "react"; import { useMutation, useQueryClient } from "react-query"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { updateOrganization, deleteOrganization, } from "api/queries/organizations"; +import type { Organization } from "api/typesGenerated"; import { EmptyState } from "components/EmptyState/EmptyState"; import { displaySuccess } from "components/GlobalSnackbar/utils"; import { useOrganizationSettings } from "./ManagementSettingsLayout"; import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView"; const OrganizationSettingsPage: FC = () => { - const navigate = useNavigate(); + const { organization: organizationName } = useParams() as { + organization?: string; + }; + const { organizations } = useOrganizationSettings(); + const navigate = useNavigate(); const queryClient = useQueryClient(); const updateOrganizationMutation = useMutation( updateOrganization(queryClient), @@ -21,14 +26,14 @@ const OrganizationSettingsPage: FC = () => { deleteOrganization(queryClient), ); - const { currentOrganizationId, organizations } = useOrganizationSettings(); - - const org = organizations.find((org) => org.id === currentOrganizationId); + const org = organizationName + ? getOrganizationByName(organizations, organizationName) + : getOrganizationByDefault(organizations); const error = updateOrganizationMutation.error ?? deleteOrganizationMutation.error; - if (!currentOrganizationId || !org) { + if (!org) { return ; } @@ -55,3 +60,9 @@ 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); diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPlaceholder.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPlaceholder.tsx deleted file mode 100644 index a1526ed53c102..0000000000000 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPlaceholder.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { FC } from "react"; -import { useMutation, useQueryClient } from "react-query"; -import { - createOrganization, - deleteOrganization, -} from "api/queries/organizations"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Margins } from "components/Margins/Margins"; -import { useOrganizationSettings } from "./ManagementSettingsLayout"; - -const OrganizationSettingsPage: FC = () => { - const queryClient = useQueryClient(); - const addOrganizationMutation = useMutation(createOrganization(queryClient)); - const deleteOrganizationMutation = useMutation( - deleteOrganization(queryClient), - ); - - const { currentOrganizationId, organizations } = useOrganizationSettings(); - - const org = organizations.find((org) => org.id === currentOrganizationId)!; - - const error = - addOrganizationMutation.error ?? deleteOrganizationMutation.error; - - return ( - - {Boolean(error) && } - -

Organization settings

- -

Name: {org.name}

-

Display name: {org.display_name}

-
- ); -}; - -export default OrganizationSettingsPage; diff --git a/site/src/pages/ManagementSettingsPage/Sidebar.tsx b/site/src/pages/ManagementSettingsPage/Sidebar.tsx index fe34a6088a01b..a07da66570897 100644 --- a/site/src/pages/ManagementSettingsPage/Sidebar.tsx +++ b/site/src/pages/ManagementSettingsPage/Sidebar.tsx @@ -3,44 +3,63 @@ import type { Interpolation, Theme } from "@emotion/react"; import AddIcon from "@mui/icons-material/Add"; import SettingsIcon from "@mui/icons-material/Settings"; import type { FC, ReactNode } from "react"; -import { Link, NavLink, useLocation } from "react-router-dom"; +import { Link, NavLink, useLocation, useParams } from "react-router-dom"; import type { Organization } from "api/typesGenerated"; import { Sidebar as BaseSidebar } from "components/Sidebar/Sidebar"; import { Stack } from "components/Stack/Stack"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { type ClassName, useClassName } from "hooks/useClassName"; +import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { USERS_LINK } from "modules/navigation"; import { useOrganizationSettings } from "./ManagementSettingsLayout"; export const Sidebar: FC = () => { - const { currentOrganizationId, organizations } = useOrganizationSettings(); + const { organizations } = useOrganizationSettings(); + const { organization = getOrganizationNameByDefault(organizations) } = + useParams() as { organization: string }; + const { multiple_organizations: organizationsEnabled } = + useFeatureVisibility(); // TODO: Do something nice to scroll to the active org. return ( -
Deployment
- -
Organizations
- } - > - New organization - - {organizations.map((organization) => ( - - ))} + {organizationsEnabled && ( +
Deployment
+ )} + + {organizationsEnabled && ( + <> +
Organizations
+ } + > + New organization + + {organizations.map((org) => ( + + ))} + + )}
); }; -const DeploymentSettingsNavigation: FC = () => { +interface DeploymentSettingsNavigationProps { + organizationsEnabled?: boolean; +} + +const DeploymentSettingsNavigation: FC = ({ + organizationsEnabled, +}) => { const location = useLocation(); const active = location.pathname.startsWith("/deployment"); @@ -81,6 +100,9 @@ const DeploymentSettingsNavigation: FC = () => { Users + {!organizationsEnabled && ( + Groups + )} )} @@ -259,3 +281,6 @@ const classNames = { font-weight: 600; `, } satisfies Record; + +const getOrganizationNameByDefault = (organizations: Organization[]) => + organizations.find((org) => org.is_default)?.name; diff --git a/site/src/router.tsx b/site/src/router.tsx index c66a98b8a9a6e..3d54613fb98dd 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -242,10 +242,6 @@ const OrganizationGroupSettingsPage = lazy( const OrganizationMembersPage = lazy( () => import("./pages/ManagementSettingsPage/OrganizationMembersPage"), ); -const OrganizationSettingsPlaceholder = lazy( - () => - import("./pages/ManagementSettingsPage/OrganizationSettingsPlaceholder"), -); const TemplateEmbedPage = lazy( () => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"), ); @@ -275,6 +271,21 @@ const RoutesWithSuspense = () => { ); }; +const groupsRouter = () => { + return ( + + } /> + + } /> + } /> + } + /> + + ); +}; + export const router = createBrowserRouter( createRoutesFromChildren( }> @@ -360,23 +371,8 @@ export const router = createBrowserRouter( } /> } /> - - } /> - - } - /> - } /> - } - /> - - } - /> + {groupsRouter()} + } /> @@ -409,6 +405,7 @@ export const router = createBrowserRouter( } /> } /> } /> + {groupsRouter()} }>