fix: disable sharing ui when sharing is unavailable (#22390)

Currently the sharing UI is only hidden under certain circumstances,
rather than on a permission basis. This makes it permissions based, and
makes some backend changes to make sure permissions are correct.
This commit is contained in:
Kayla はな
2026-03-02 19:04:55 -07:00
committed by GitHub
parent b7a7683ac0
commit 2bdf80d452
29 changed files with 181 additions and 138 deletions
+3 -2
View File
@@ -333,9 +333,10 @@ func New(options *Options) *API {
panic("developer error: options.PrometheusRegistry is nil and not running a unit test")
}
if options.DeploymentValues.DisableOwnerWorkspaceExec {
if options.DeploymentValues.DisableOwnerWorkspaceExec || options.DeploymentValues.DisableWorkspaceSharing {
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
NoOwnerWorkspaceExec: true,
NoOwnerWorkspaceExec: bool(options.DeploymentValues.DisableOwnerWorkspaceExec),
NoWorkspaceSharing: bool(options.DeploymentValues.DisableWorkspaceSharing),
})
}
+38 -18
View File
@@ -244,6 +244,7 @@ func SystemRoleName(name string) bool {
type RoleOptions struct {
NoOwnerWorkspaceExec bool
NoWorkspaceSharing bool
}
// ReservedRoleName exists because the database should only allow unique role
@@ -267,12 +268,23 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
opts = &RoleOptions{}
}
denyPermissions := []Permission{}
if opts.NoWorkspaceSharing {
denyPermissions = append(denyPermissions, Permission{
Negate: true,
ResourceType: ResourceWorkspace.Type,
Action: policy.ActionShare,
})
}
ownerWorkspaceActions := ResourceWorkspace.AvailableActions()
if opts.NoOwnerWorkspaceExec {
// Remove ssh and application connect from the owner role. This
// prevents owners from have exec access to all workspaces.
ownerWorkspaceActions = slice.Omit(ownerWorkspaceActions,
policy.ActionApplicationConnect, policy.ActionSSH)
ownerWorkspaceActions = slice.Omit(
ownerWorkspaceActions,
policy.ActionApplicationConnect, policy.ActionSSH,
)
}
// Static roles that never change should be allocated in a closure.
@@ -295,7 +307,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// Explicitly setting PrebuiltWorkspace permissions for clarity.
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
})...),
})...,
),
User: []Permission{},
ByOrgID: map[string]OrgPermissions{},
}.withCachedRegoValue()
@@ -303,13 +316,17 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
memberRole := Role{
Identifier: RoleMember(),
DisplayName: "Member",
Site: Permissions(map[string][]policy.Action{
ResourceAssignRole.Type: {policy.ActionRead},
// All users can see OAuth2 provider applications.
ResourceOauth2App.Type: {policy.ActionRead},
ResourceWorkspaceProxy.Type: {policy.ActionRead},
}),
User: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage),
Site: append(
Permissions(map[string][]policy.Action{
ResourceAssignRole.Type: {policy.ActionRead},
// All users can see OAuth2 provider applications.
ResourceOauth2App.Type: {policy.ActionRead},
ResourceWorkspaceProxy.Type: {policy.ActionRead},
}),
denyPermissions...,
),
User: append(
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage),
Permissions(map[string][]policy.Action{
// Users cannot do create/update/delete on themselves, but they
// can read their own details.
@@ -433,14 +450,17 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ByOrgID: map[string]OrgPermissions{
// Org admins should not have workspace exec perms.
organizationID.String(): {
Org: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage), Permissions(map[string][]policy.Action{
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
// PrebuiltWorkspaces are a subset of Workspaces.
// Explicitly setting PrebuiltWorkspace permissions for clarity.
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
})...),
Org: append(
allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage),
Permissions(map[string][]policy.Action{
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
// PrebuiltWorkspaces are a subset of Workspaces.
// Explicitly setting PrebuiltWorkspace permissions for clarity.
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
})...,
),
Member: []Permission{},
},
},
+4
View File
@@ -5572,6 +5572,10 @@ func TestWorkspaceSharingDisabled(t *testing.T) {
})
t.Run("NoAccessWhenDisabled", func(t *testing.T) {
t.Cleanup(func() {
rbac.ReloadBuiltinRoles(nil)
})
var (
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
+1 -1
View File
@@ -248,7 +248,7 @@ export const patchRoleSyncSettings = (
};
};
const getWorkspaceSharingSettingsKey = (organization: string) => [
export const getWorkspaceSharingSettingsKey = (organization: string) => [
"organization",
organization,
"workspaceSharingSettings",
+1 -1
View File
@@ -489,7 +489,7 @@ export const workspacePermissions = (workspace?: Workspace) => {
checks: workspace ? workspaceChecks(workspace) : {},
}),
queryKey: ["workspaces", workspace?.id, "permissions"],
enabled: !!workspace,
enabled: Boolean(workspace),
staleTime: Number.POSITIVE_INFINITY,
};
};
@@ -1,3 +1,4 @@
import { workspaceSharingSettings } from "api/queries/organizations";
import type {
Group,
WorkspaceACL,
@@ -37,6 +38,7 @@ import { TableLoader } from "components/TableLoader/TableLoader";
import { EllipsisVertical, UserPlusIcon } from "lucide-react";
import { getGroupSubtitle } from "modules/groups";
import type { FC, ReactNode } from "react";
import { useQuery } from "react-query";
interface RoleSelectProps {
value: WorkspaceRole;
@@ -139,6 +141,7 @@ export const RoleSelectField: FC<RoleSelectFieldProps> = ({
};
interface WorkspaceSharingFormProps {
organizationId: string;
workspaceACL: WorkspaceACL | undefined;
canUpdatePermissions: boolean;
isTaskWorkspace: boolean;
@@ -155,6 +158,7 @@ interface WorkspaceSharingFormProps {
}
export const WorkspaceSharingForm: FC<WorkspaceSharingFormProps> = ({
organizationId,
workspaceACL,
canUpdatePermissions,
isTaskWorkspace,
@@ -169,6 +173,46 @@ export const WorkspaceSharingForm: FC<WorkspaceSharingFormProps> = ({
isCompact,
showRestartWarning,
}) => {
const sharingSettingsQuery = useQuery(
workspaceSharingSettings(organizationId),
);
if (sharingSettingsQuery.isLoading) {
return (
<TableBody>
<TableLoader />
</TableBody>
);
}
if (!sharingSettingsQuery.data) {
return (
<TableBody>
<TableRow>
<TableCell colSpan={999}>
<ErrorAlert error={sharingSettingsQuery.error} />
</TableCell>
</TableRow>
</TableBody>
);
}
if (sharingSettingsQuery.data.sharing_disabled) {
return (
<TableBody>
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="This workspace cannot be shared"
description="Workspace sharing has been disabled for this organization."
isCompact={isCompact}
/>
</TableCell>
</TableRow>
</TableBody>
);
}
const isEmpty = Boolean(
workspaceACL &&
workspaceACL.users.length === 0 &&
+8 -8
View File
@@ -10,6 +10,14 @@ export const workspaceChecks = (workspace: Workspace) =>
},
action: "read",
},
shareWorkspace: {
object: {
resource_type: "workspace",
resource_id: workspace.id,
owner_id: workspace.owner_id,
},
action: "share",
},
updateWorkspace: {
object: {
resource_type: "workspace",
@@ -34,14 +42,6 @@ export const workspaceChecks = (workspace: Workspace) =>
},
action: "update",
},
// To run a build in debug mode we need to be able to read the deployment
// config (enable_terraform_debug_mode).
deploymentConfig: {
object: {
resource_type: "deployment_config",
},
action: "read",
},
}) satisfies Record<string, AuthorizationCheck>;
export type WorkspacePermissions = Record<
@@ -24,9 +24,9 @@ const createTimestamp = (
const permissions: WorkspacePermissions = {
readWorkspace: true,
shareWorkspace: true,
updateWorkspace: true,
updateWorkspaceVersion: true,
deploymentConfig: true,
deleteFailedWorkspace: true,
};
@@ -57,7 +57,6 @@ export const Workspace: FC<WorkspaceProps> = ({
latestVersion,
permissions,
timings,
sharingDisabled,
handleStart,
handleStop,
handleRestart,
@@ -111,7 +110,6 @@ export const Workspace: FC<WorkspaceProps> = ({
latestVersion={latestVersion}
isUpdating={isUpdating}
isRestarting={isRestarting}
sharingDisabled={sharingDisabled}
handleStart={handleStart}
handleStop={handleStop}
handleRestart={handleRestart}
@@ -38,6 +38,7 @@ export const ShareButton: FC<ShareButtonProps> = ({
<FeatureStageBadge contentType="beta" size="sm" />
</div>
<WorkspaceSharingForm
organizationId={workspace.organization_id}
workspaceACL={sharing.workspaceACL}
canUpdatePermissions={canUpdatePermissions}
isTaskWorkspace={Boolean(workspace.task_id)}
@@ -16,11 +16,11 @@ const meta: Meta<typeof WorkspaceActions> = {
args: {
isUpdating: false,
permissions: {
deleteFailedWorkspace: true,
deploymentConfig: true,
readWorkspace: true,
shareWorkspace: true,
updateWorkspace: true,
updateWorkspaceVersion: true,
deleteFailedWorkspace: true,
},
},
decorators: [withDashboardProvider, withDesktopViewport, withAuthProvider],
@@ -172,11 +172,11 @@ export const FailedWithDebug: Story = {
args: {
workspace: Mocks.MockFailedWorkspace,
permissions: {
deploymentConfig: true,
deleteFailedWorkspace: true,
readWorkspace: true,
shareWorkspace: true,
updateWorkspace: true,
updateWorkspaceVersion: true,
deleteFailedWorkspace: true,
},
},
};
@@ -29,7 +29,6 @@ interface WorkspaceActionsProps {
isUpdating: boolean;
isRestarting: boolean;
permissions: WorkspacePermissions;
sharingDisabled?: boolean;
handleToggleFavorite: () => void;
handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void;
handleStop: () => void;
@@ -46,7 +45,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
isUpdating,
isRestarting,
permissions,
sharingDisabled,
handleToggleFavorite,
handleStart,
handleStop,
@@ -57,10 +55,13 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
handleDebug,
handleDormantActivate,
}) => {
const { user } = useAuthenticated();
const {
permissions: { viewDeploymentConfig },
user,
} = useAuthenticated();
const { data: deployment } = useQuery({
...deploymentConfig(),
enabled: permissions.deploymentConfig,
enabled: viewDeploymentConfig,
});
const { actions, canCancel, canAcceptJobs } = abilitiesByWorkspaceStatus(
workspace,
@@ -191,7 +192,7 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
onToggle={handleToggleFavorite}
/>
{!sharingDisabled && (
{permissions.shareWorkspace && (
<ShareButton
workspace={workspace}
canUpdatePermissions={permissions.updateWorkspace}
@@ -14,9 +14,9 @@ import { WorkspaceNotifications } from "./WorkspaceNotifications";
export const defaultPermissions: WorkspacePermissions = {
readWorkspace: true,
updateWorkspaceVersion: true,
shareWorkspace: true,
updateWorkspace: true,
deploymentConfig: true,
updateWorkspaceVersion: true,
deleteFailedWorkspace: true,
};
@@ -125,11 +125,11 @@ describe("WorkspacePage", () => {
server.use(
http.post("/api/v2/authcheck", async () => {
const permissions: WorkspacePermissions = {
deleteFailedWorkspace: true,
deploymentConfig: true,
readWorkspace: true,
shareWorkspace: true,
updateWorkspace: true,
updateWorkspaceVersion: true,
deleteFailedWorkspace: true,
};
return HttpResponse.json(permissions);
}),
@@ -1,5 +1,4 @@
import { watchWorkspace } from "api/api";
import { workspaceSharingSettings } from "api/queries/organizations";
import { template as templateQueryOptions } from "api/queries/templates";
import { workspaceBuildsKey } from "api/queries/workspaceBuilds";
import {
@@ -45,12 +44,6 @@ const WorkspacePage: FC = () => {
const permissionsQuery = useQuery(workspacePermissions(workspace));
const permissions = permissionsQuery.data;
const sharingSettingsQuery = useQuery({
...workspaceSharingSettings(workspace?.organization_id ?? ""),
enabled: !!workspace,
});
const sharingDisabled = sharingSettingsQuery.data?.sharing_disabled ?? false;
// Watch workspace changes
const updateWorkspaceData = useEffectEvent(
async (newWorkspaceData: Workspace) => {
@@ -124,7 +117,6 @@ const WorkspacePage: FC = () => {
workspace={workspace}
template={template}
permissions={permissions}
sharingDisabled={sharingDisabled}
/>
);
};
@@ -39,14 +39,12 @@ interface WorkspaceReadyPageProps {
template: TypesGen.Template;
workspace: TypesGen.Workspace;
permissions: WorkspacePermissions;
sharingDisabled?: boolean;
}
export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
workspace,
template,
permissions,
sharingDisabled,
}) => {
const queryClient = useQueryClient();
@@ -298,7 +296,6 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
template={template}
buildLogs={buildLogs}
timings={timingsQuery.data}
sharingDisabled={sharingDisabled}
handleStart={async (buildParameters) => {
const { hasEphemeral, ephemeralParameters } =
await checkEphemeralParameters(buildParameters);
@@ -35,9 +35,9 @@ const meta: Meta<typeof WorkspaceTopbar> = {
latestVersion: MockTemplateVersion,
permissions: {
readWorkspace: true,
updateWorkspaceVersion: true,
shareWorkspace: true,
updateWorkspace: true,
deploymentConfig: true,
updateWorkspaceVersion: true,
deleteFailedWorkspace: true,
},
},
@@ -44,7 +44,6 @@ interface WorkspaceProps {
template: TypesGen.Template;
permissions: WorkspacePermissions;
latestVersion?: TypesGen.TemplateVersion;
sharingDisabled?: boolean;
handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
handleStop: () => void;
handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
@@ -63,7 +62,6 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
permissions,
isUpdating,
isRestarting,
sharingDisabled,
handleStart,
handleStop,
handleRestart,
@@ -238,7 +236,6 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
permissions={permissions}
isUpdating={isUpdating}
isRestarting={isRestarting}
sharingDisabled={sharingDisabled}
handleStart={handleStart}
handleStop={handleStop}
handleRestart={handleRestart}
@@ -1,4 +1,3 @@
import type { Workspace } from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import {
@@ -12,19 +11,11 @@ import {
TimerIcon as ScheduleIcon,
Users as SharingIcon,
} from "lucide-react";
import type { FC } from "react";
import { useWorkspaceSettings } from "./WorkspaceSettingsLayout";
interface SidebarProps {
username: string;
workspace: Workspace;
sharingDisabled?: boolean;
}
export const Sidebar: React.FC = () => {
const { owner, workspace, permissions } = useWorkspaceSettings();
export const Sidebar: FC<SidebarProps> = ({
username,
workspace,
sharingDisabled,
}) => {
return (
<BaseSidebar>
<SidebarHeader
@@ -36,7 +27,7 @@ export const Sidebar: FC<SidebarProps> = ({
/>
}
title={workspace.name}
linkTo={`/@${username}/${workspace.name}`}
linkTo={`/@${owner}/${workspace.name}`}
subtitle={workspace.template_display_name ?? workspace.template_name}
/>
@@ -49,7 +40,7 @@ export const Sidebar: FC<SidebarProps> = ({
<SidebarNavItem href="schedule" icon={ScheduleIcon}>
Schedule
</SidebarNavItem>
{!sharingDisabled && (
{permissions?.shareWorkspace && (
<SidebarNavItem href="sharing" icon={SharingIcon}>
Sharing
<FeatureStageBadge contentType="beta" size="sm" />
@@ -4,7 +4,7 @@ import WorkspaceParametersPage from "./WorkspaceParametersPage";
import WorkspaceParametersPageExperimental from "./WorkspaceParametersPageExperimental";
const WorkspaceParametersExperimentRouter: FC = () => {
const workspace = useWorkspaceSettings();
const { workspace } = useWorkspaceSettings();
return (
<>
@@ -29,7 +29,7 @@ import {
} from "./WorkspaceParametersForm";
const WorkspaceParametersPage: FC = () => {
const workspace = useWorkspaceSettings();
const { workspace } = useWorkspaceSettings();
const build = workspace.latest_build;
const { data: templateVersionParameters } = useQuery(
richParameters(build.template_version_id),
@@ -33,7 +33,7 @@ import { useWorkspaceSettings } from "../WorkspaceSettingsLayout";
import { WorkspaceParametersPageViewExperimental } from "./WorkspaceParametersPageViewExperimental";
const WorkspaceParametersPageExperimental: FC = () => {
const workspace = useWorkspaceSettings();
const { workspace } = useWorkspaceSettings();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const templateVersionId = searchParams.get("templateVersionId") ?? undefined;
@@ -6,10 +6,10 @@ import {
} from "testHelpers/entities";
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { getAuthorizationKey } from "api/queries/authCheck";
import { templateByNameKey } from "api/queries/templates";
import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
import type { Workspace } from "api/typesGenerated";
import type { WorkspacePermissions } from "modules/workspaces/permissions";
import {
reactRouterOutlet,
reactRouterParameters,
@@ -68,19 +68,14 @@ function workspaceQueries(workspace: Workspace) {
data: workspace,
},
{
key: getAuthorizationKey({
checks: {
updateWorkspace: {
object: {
resource_type: "workspace",
resource_id: MockWorkspace.id,
owner_id: MockWorkspace.owner_id,
},
action: "update",
},
},
}),
data: { updateWorkspace: true },
key: ["workspaces", workspace.id, "permissions"],
data: {
readWorkspace: true,
shareWorkspace: true,
updateWorkspace: true,
updateWorkspaceVersion: true,
deleteFailedWorkspace: true,
} satisfies WorkspacePermissions,
},
{
key: templateByNameKey(
@@ -1,6 +1,5 @@
import { API } from "api/api";
import { getErrorDetail } from "api/errors";
import { checkAuthorization } from "api/queries/authCheck";
import { templateByName } from "api/queries/templates";
import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
import type * as TypesGen from "api/typesGenerated";
@@ -29,28 +28,13 @@ import {
} from "./formToRequest";
import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm";
const permissionsToCheck = (workspace: TypesGen.Workspace) =>
({
updateWorkspace: {
object: {
resource_type: "workspace",
resource_id: workspace.id,
owner_id: workspace.owner_id,
},
action: "update",
},
}) as const;
const WorkspaceSchedulePage: FC = () => {
const params = useParams() as { username: string; workspace: string };
const navigate = useNavigate();
const username = params.username.replace("@", "");
const workspaceName = params.workspace;
const queryClient = useQueryClient();
const workspace = useWorkspaceSettings();
const { data: permissions, error: checkPermissionsError } = useQuery(
checkAuthorization({ checks: permissionsToCheck(workspace) }),
);
const { permissions, workspace } = useWorkspaceSettings();
const { data: template, error: getTemplateError } = useQuery(
templateByName(workspace.organization_id, workspace.template_name),
);
@@ -75,8 +59,8 @@ const WorkspaceSchedulePage: FC = () => {
},
),
});
const error = checkPermissionsError || getTemplateError;
const isLoading = !template || !permissions;
const error = getTemplateError;
const isLoading = !template;
const [isConfirmingApply, setIsConfirmingApply] = useState(false);
const { mutate: updateWorkspace } = useMutation({
@@ -1,17 +1,28 @@
import { workspaceSharingSettings } from "api/queries/organizations";
import { workspaceByOwnerAndName } from "api/queries/workspaces";
import {
workspaceByOwnerAndName,
workspacePermissions,
} from "api/queries/workspaces";
import type { Workspace } 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";
import type { WorkspacePermissions } from "modules/workspaces/permissions";
import { createContext, type FC, Suspense, useContext } from "react";
import { useQuery } from "react-query";
import { Outlet, useParams } from "react-router";
import { pageTitle } from "utils/page";
import { Sidebar } from "./Sidebar";
const WorkspaceSettings = createContext<Workspace | undefined>(undefined);
type WorkspaceSettingsContext = {
owner: string;
workspace: Workspace;
permissions?: WorkspacePermissions;
};
const WorkspaceSettings = createContext<WorkspaceSettingsContext | undefined>(
undefined,
);
export function useWorkspaceSettings() {
const value = useContext(WorkspaceSettings);
@@ -31,39 +42,36 @@ export const WorkspaceSettingsLayout: FC = () => {
};
const workspaceName = params.workspace;
const username = params.username.replace("@", "");
const {
data: workspace,
error,
isLoading,
isError,
} = useQuery(workspaceByOwnerAndName(username, workspaceName));
const workspaceQuery = useQuery(
workspaceByOwnerAndName(username, workspaceName),
);
const sharingSettingsQuery = useQuery({
...workspaceSharingSettings(workspace?.organization_id ?? ""),
enabled: !!workspace,
});
const sharingDisabled = sharingSettingsQuery.data?.sharing_disabled ?? false;
const permissionsQuery = useQuery(workspacePermissions(workspaceQuery.data));
if (isLoading) {
if (workspaceQuery.isLoading) {
return <Loader />;
}
const error = workspaceQuery.error || permissionsQuery.error;
return (
<>
<title>{pageTitle(workspaceName, "Settings")}</title>
<Margins>
<Stack css={{ padding: "48px 0" }} direction="row" spacing={10}>
{isError ? (
{error ? (
<ErrorAlert error={error} />
) : (
workspace && (
<WorkspaceSettings.Provider value={workspace}>
<Sidebar
workspace={workspace}
username={username}
sharingDisabled={sharingDisabled}
/>
workspaceQuery.data && (
<WorkspaceSettings.Provider
value={{
owner: username,
workspace: workspaceQuery.data,
permissions: permissionsQuery.data,
}}
>
<Sidebar />
<Suspense fallback={<Loader />}>
<main css={{ width: "100%" }}>
<Outlet />
@@ -15,7 +15,7 @@ const WorkspaceSettingsPage: FC = () => {
};
const workspaceName = params.workspace;
const username = params.username.replace("@", "");
const workspace = useWorkspaceSettings();
const { workspace } = useWorkspaceSettings();
const navigate = useNavigate();
const mutation = useMutation({
@@ -11,7 +11,7 @@ import { useWorkspaceSettings } from "../WorkspaceSettingsLayout";
import { WorkspaceSharingPageView } from "./WorkspaceSharingPageView";
const WorkspaceSharingPage: FC = () => {
const workspace = useWorkspaceSettings();
const { workspace } = useWorkspaceSettings();
const sharing = useWorkspaceSharing(workspace);
const checks = workspaceChecks(workspace);
@@ -25,7 +25,7 @@ const WorkspaceSharingPage: FC = () => {
sharing.error ?? permissionsQuery.error ?? sharing.mutationError;
return (
<div className="flex flex-col gap-12 max-w-screen-md">
<div className="flex flex-col gap-12">
<title>{pageTitle(workspace.name, "Sharing")}</title>
<header className="flex flex-col">
@@ -7,6 +7,7 @@ import {
mockApiError,
} from "testHelpers/entities";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { getWorkspaceSharingSettingsKey } from "api/queries/organizations";
import type {
WorkspaceACL,
WorkspaceGroup,
@@ -63,6 +64,14 @@ const aclWithUsersAndGroups: WorkspaceACL = {
const meta: Meta<typeof WorkspaceSharingPageView> = {
title: "pages/WorkspaceSharingPageView",
component: WorkspaceSharingPageView,
parameters: {
queries: [
{
key: getWorkspaceSharingSettingsKey(MockWorkspace.organization_id),
data: { sharing_disabled: false },
},
],
},
args: {
workspace: MockWorkspace,
workspaceACL: emptyACL,
@@ -52,6 +52,7 @@ export const WorkspaceSharingPageView: FC<WorkspaceSharingPageViewProps> = ({
}) => {
return (
<WorkspaceSharingForm
organizationId={workspace.organization_id}
workspaceACL={workspaceACL}
canUpdatePermissions={canUpdatePermissions}
isTaskWorkspace={Boolean(workspace.task_id)}