From f36f1067eb70f2d9baad81e18ee6e789be98a972 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 3 Jun 2024 21:16:40 +0000 Subject: [PATCH 01/19] rebased old version --- site/src/api/api.ts | 14 +++++++ site/src/api/queries/organizations.ts | 27 ++++++++++++ site/src/api/queries/users.ts | 6 ++- site/src/pages/DeploySettingsPage/Sidebar.tsx | 4 ++ .../TeamsSettingsPage/TeamsSettingsPage.tsx | 42 +++++++++++++++++++ site/src/router.tsx | 5 +++ 6 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 site/src/api/queries/organizations.ts create mode 100644 site/src/pages/DeploySettingsPage/TeamsSettingsPage/TeamsSettingsPage.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 7e8829201dc3a..fc79dcb458c30 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -505,6 +505,20 @@ class ApiMethods { return response.data; }; + createOrganization = async (params: TypesGen.CreateOrganizationRequest) => { + const response = await this.axios.post( + "/api/v2/organizations", + params, + ); + return response.data; + }; + + deleteOrganization = async (orgId: string) => { + await this.axios.delete( + `/api/v2/organizations/${orgId}`, + ); + }; + getOrganization = async ( organizationId: string, ): Promise => { diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts new file mode 100644 index 0000000000000..1689f841aada4 --- /dev/null +++ b/site/src/api/queries/organizations.ts @@ -0,0 +1,27 @@ +import type { QueryClient } from "react-query"; +import { API } from "api/api"; +import type { CreateOrganizationRequest } from "api/typesGenerated"; +import { meKey, myOrganizationsKey } from "./users"; + +export const createOrganization = (queryClient: QueryClient) => { + return { + mutationFn: (params: CreateOrganizationRequest) => + API.createOrganization(params), + + onSuccess: async () => { + await queryClient.invalidateQueries(meKey); + await queryClient.invalidateQueries(myOrganizationsKey); + }, + }; +}; + +export const deleteOrganization = (queryClient: QueryClient) => { + return { + mutationFn: (orgId: string) => API.deleteOrganization(orgId), + + onSuccess: async () => { + await queryClient.invalidateQueries(meKey); + await queryClient.invalidateQueries(myOrganizationsKey); + }, + }; +}; diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index cf70038e7ca23..db43fa46620f5 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -124,7 +124,7 @@ export const authMethods = () => { }; }; -const meKey = ["me"]; +export const meKey = ["me"]; export const me = (metadata: MetadataState) => { return cachedQuery({ @@ -250,9 +250,11 @@ export const updateAppearanceSettings = ( }; }; +export const myOrganizationsKey = ["organizations", "me"] as const; + export const myOrganizations = () => { return { - queryKey: ["organizations", "me"], + queryKey: myOrganizationsKey, queryFn: () => API.getOrganizations(), }; }; diff --git a/site/src/pages/DeploySettingsPage/Sidebar.tsx b/site/src/pages/DeploySettingsPage/Sidebar.tsx index e473ab94ca510..72da01d449794 100644 --- a/site/src/pages/DeploySettingsPage/Sidebar.tsx +++ b/site/src/pages/DeploySettingsPage/Sidebar.tsx @@ -1,4 +1,5 @@ import Brush from "@mui/icons-material/Brush"; +import TeamsIcon from "@mui/icons-material/Groups"; import HubOutlinedIcon from "@mui/icons-material/HubOutlined"; import InsertChartIcon from "@mui/icons-material/InsertChart"; import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; @@ -22,6 +23,9 @@ export const Sidebar: FC = () => { Licenses + + Teams + Appearance diff --git a/site/src/pages/DeploySettingsPage/TeamsSettingsPage/TeamsSettingsPage.tsx b/site/src/pages/DeploySettingsPage/TeamsSettingsPage/TeamsSettingsPage.tsx new file mode 100644 index 0000000000000..62e3defe2d098 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/TeamsSettingsPage/TeamsSettingsPage.tsx @@ -0,0 +1,42 @@ +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import { type FC, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { + createOrganization, + deleteOrganization, +} from "api/queries/organizations"; +import { myOrganizations } from "api/queries/users"; + +const TeamsSettingsPage: FC = () => { + const queryClient = useQueryClient(); + const addTeamMutation = useMutation(createOrganization(queryClient)); + const deleteTeamMutation = useMutation(deleteOrganization(queryClient)); + const organizationsQuery = useQuery(myOrganizations()); + const [newOrgName, setNewOrgName] = useState(""); + return ( + <> + setNewOrgName(event.target.value)} + /> +

{String(addTeamMutation.error)}

+ + +
{String(deleteTeamMutation.error)}
+ + {organizationsQuery.data?.map((org) => ( +
+ {org.name}{" "} + +
+ ))} + + ); +}; + +export default TeamsSettingsPage; diff --git a/site/src/router.tsx b/site/src/router.tsx index de288d37d3941..8abd57726e4cf 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -220,6 +220,10 @@ const AddNewLicensePage = lazy( () => import("./pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage"), ); +const TeamsSettingsPage = lazy( + () => + import("./pages/DeploySettingsPage/TeamsSettingsPage/TeamsSettingsPage"), +); const TemplateEmbedPage = lazy( () => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"), ); @@ -329,6 +333,7 @@ export const router = createBrowserRouter( } /> } /> } /> + } /> } /> Date: Mon, 3 Jun 2024 21:43:14 +0000 Subject: [PATCH 02/19] move out of deployment settings --- .../dashboard/Navbar/DeploymentDropdown.tsx | 23 +++++++++++++++---- .../modules/dashboard/Navbar/NavbarView.tsx | 1 + .../OrganizationSettingsPage.tsx} | 0 site/src/router.tsx | 8 +++---- 4 files changed, 24 insertions(+), 8 deletions(-) rename site/src/pages/{DeploySettingsPage/TeamsSettingsPage/TeamsSettingsPage.tsx => OrganizationSettingsPage/OrganizationSettingsPage.tsx} (100%) diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index 23f0355ad3e9a..7c99fae34f546 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -13,22 +13,25 @@ import { import { USERS_LINK } from "modules/navigation"; interface DeploymentDropdownProps { - canViewAuditLog: boolean; canViewDeployment: boolean; + canViewOrganizations: boolean; canViewAllUsers: boolean; + canViewAuditLog: boolean; canViewHealth: boolean; } export const DeploymentDropdown: FC = ({ - canViewAuditLog, canViewDeployment, + canViewOrganizations, canViewAllUsers, + canViewAuditLog, canViewHealth, }) => { const theme = useTheme(); if ( !canViewAuditLog && + !canViewOrganizations && !canViewDeployment && !canViewAllUsers && !canViewHealth @@ -64,9 +67,10 @@ export const DeploymentDropdown: FC = ({ }} > @@ -75,9 +79,10 @@ export const DeploymentDropdown: FC = ({ }; const DeploymentDropdownContent: FC = ({ - canViewAuditLog, canViewDeployment, + canViewOrganizations, canViewAllUsers, + canViewAuditLog, canViewHealth, }) => { const popover = usePopover(); @@ -96,6 +101,16 @@ const DeploymentDropdownContent: FC = ({ Settings )} + {canViewDeployment && ( + + Organizations + + )} {canViewAllUsers && ( = ({ import("./pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage"), ); -const TeamsSettingsPage = lazy( - () => - import("./pages/DeploySettingsPage/TeamsSettingsPage/TeamsSettingsPage"), +const OrganizationSettingsPage = lazy( + () => import("./pages/OrganizationSettingsPage/OrganizationSettingsPage"), ); const TemplateEmbedPage = lazy( () => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"), @@ -329,11 +328,12 @@ export const router = createBrowserRouter( } /> + } /> + }> } /> } /> } /> - } /> } /> Date: Tue, 4 Jun 2024 21:20:08 +0000 Subject: [PATCH 03/19] padding and cleanup --- site/src/components/Margins/Margins.tsx | 14 +++++++-- .../dashboard/Navbar/DeploymentDropdown.tsx | 2 +- site/src/pages/DeploySettingsPage/Sidebar.tsx | 3 -- .../OrganizationSettingsPage.tsx | 31 ++++++++++++------- .../OrganizationSettingsPage/Sidebar.tsx | 21 +++++++++++++ 5 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 site/src/pages/OrganizationSettingsPage/Sidebar.tsx diff --git a/site/src/components/Margins/Margins.tsx b/site/src/components/Margins/Margins.tsx index 9c03d2626174d..49f55199f09b6 100644 --- a/site/src/components/Margins/Margins.tsx +++ b/site/src/components/Margins/Margins.tsx @@ -13,8 +13,15 @@ const widthBySize: Record = { small: containerWidth / 3, }; -export const Margins: FC = ({ +type MarginsProps = JSX.IntrinsicElements["div"] & { + size?: Size; + verticalMargin?: string | number; +}; + +export const Margins: FC = ({ size = "regular", + verticalMargin = 0, + children, ...divProps }) => { const maxWidth = widthBySize[size]; @@ -27,6 +34,9 @@ export const Margins: FC = ({ padding: `0 ${sidePadding}px`, width: "100%", }} - /> + style={{ marginTop: verticalMargin, marginBottom: verticalMargin }} + > + {children} + ); }; diff --git a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx index 7c99fae34f546..e54210d831d8e 100644 --- a/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx @@ -101,7 +101,7 @@ const DeploymentDropdownContent: FC = ({ Settings )} - {canViewDeployment && ( + {canViewOrganizations && ( { Licenses - - Teams - Appearance diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 62e3defe2d098..a58500c939e01 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -7,36 +7,45 @@ import { deleteOrganization, } from "api/queries/organizations"; import { myOrganizations } from "api/queries/users"; +import { Margins } from "components/Margins/Margins"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; -const TeamsSettingsPage: FC = () => { +const OrganizationSettingsPage: FC = () => { const queryClient = useQueryClient(); - const addTeamMutation = useMutation(createOrganization(queryClient)); - const deleteTeamMutation = useMutation(deleteOrganization(queryClient)); + const addOrganizationMutation = useMutation(createOrganization(queryClient)); + const deleteOrganizationMutation = useMutation( + deleteOrganization(queryClient), + ); const organizationsQuery = useQuery(myOrganizations()); const [newOrgName, setNewOrgName] = useState(""); + + const error = + addOrganizationMutation.error ?? deleteOrganizationMutation.error; + return ( - <> + + {Boolean(error) && } + setNewOrgName(event.target.value)} /> -

{String(addTeamMutation.error)}

- -
{String(deleteTeamMutation.error)}
- {organizationsQuery.data?.map((org) => (
{org.name}{" "} -
))} - +
); }; -export default TeamsSettingsPage; +export default OrganizationSettingsPage; diff --git a/site/src/pages/OrganizationSettingsPage/Sidebar.tsx b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx new file mode 100644 index 0000000000000..04129a827fe8e --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx @@ -0,0 +1,21 @@ +import GeneralIcon from "@mui/icons-material/SettingsOutlined"; +import type { FC } from "react"; +import type { Template } from "api/typesGenerated"; +import { + Sidebar as BaseSidebar, + SidebarNavItem, +} from "components/Sidebar/Sidebar"; + +interface SidebarProps { + template: Template; +} + +export const Sidebar: FC = ({ template }) => { + return ( + + + General + + + ); +}; From 2cb6c5dcd2c3c73a609bfe0dba11ee479f2d7919 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 6 Jun 2024 17:02:45 +0000 Subject: [PATCH 04/19] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/pages/DeploySettingsPage/Sidebar.tsx | 1 - .../pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/pages/DeploySettingsPage/Sidebar.tsx b/site/src/pages/DeploySettingsPage/Sidebar.tsx index 91bdf72c35d6b..e473ab94ca510 100644 --- a/site/src/pages/DeploySettingsPage/Sidebar.tsx +++ b/site/src/pages/DeploySettingsPage/Sidebar.tsx @@ -1,5 +1,4 @@ import Brush from "@mui/icons-material/Brush"; -import TeamsIcon from "@mui/icons-material/Groups"; import HubOutlinedIcon from "@mui/icons-material/HubOutlined"; import InsertChartIcon from "@mui/icons-material/InsertChart"; import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index a58500c939e01..68a7f3a86e04d 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -7,8 +7,8 @@ import { deleteOrganization, } from "api/queries/organizations"; import { myOrganizations } from "api/queries/users"; -import { Margins } from "components/Margins/Margins"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Margins } from "components/Margins/Margins"; const OrganizationSettingsPage: FC = () => { const queryClient = useQueryClient(); From b297d3d59768a3b9be978fcac60f78acf73d39a0 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 6 Jun 2024 18:30:04 +0000 Subject: [PATCH 05/19] w h o o p s --- codersdk/organizations.go | 2 +- site/src/api/typesGenerated.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index b9ff98d1a3917..7da900378a698 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -60,7 +60,7 @@ type OrganizationMember struct { type CreateOrganizationRequest struct { Name string `json:"name" validate:"required,organization_name"` // DisplayName will default to the same value as `Name` if not provided. - DisplayName string `json:"display_name" validate:"omitempty,organization_display_name"` + DisplayName string `json:"display_name,omitempty" validate:"omitempty,organization_display_name"` Description string `json:"description,omitempty"` } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index fae41504f1c34..d60fe83de6894 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -226,7 +226,7 @@ export interface CreateGroupRequest { // From codersdk/organizations.go export interface CreateOrganizationRequest { readonly name: string; - readonly display_name: string; + readonly display_name?: string; readonly description?: string; } From 0f9de62f12b805a262ff7cc4526cc72c071b699f Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 7 Jun 2024 21:07:50 +0000 Subject: [PATCH 06/19] a mostly nice org settings page --- site/src/api/api.ts | 11 ++ site/src/api/queries/organizations.ts | 22 ++- site/src/components/FormFooter/FormFooter.tsx | 22 ++- site/src/components/Sidebar/Sidebar.tsx | 62 ++++-- site/src/modules/dashboard/Navbar/Navbar.tsx | 5 +- .../dashboard/Navbar/NavbarView.test.tsx | 15 +- .../modules/dashboard/Navbar/NavbarView.tsx | 8 +- .../Navbar/UserDropdown/UserDropdown.tsx | 11 -- .../UserDropdown/UserDropdownContent.tsx | 43 ----- .../CreateTemplatePage/CreateTemplateForm.tsx | 4 +- .../OrganizationSettingsLayout.tsx | 74 ++++++++ .../OrganizationSettingsPage.tsx | 152 +++++++++++++-- .../OrganizationSettingsPlaceholder.tsx | 37 ++++ .../OrganizationSettingsPage/Sidebar.tsx | 179 ++++++++++++++++-- .../TemplateSettingsForm.tsx | 45 +++-- .../AccountPage/AccountPage.tsx | 17 -- site/src/router.tsx | 32 +++- site/src/utils/formUtils.ts | 19 +- 18 files changed, 586 insertions(+), 172 deletions(-) create mode 100644 site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx create mode 100644 site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index dfc81c4e5391e..50dbc32a1867d 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -513,6 +513,17 @@ class ApiMethods { return response.data; }; + updateOrganization = async ( + orgId: string, + params: TypesGen.UpdateOrganizationRequest, + ) => { + const response = await this.axios.patch( + `/api/v2/organizations/${orgId}`, + params, + ); + return response.data; + }; + deleteOrganization = async (orgId: string) => { await this.axios.delete( `/api/v2/organizations/${orgId}`, diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 1689f841aada4..836a452431d83 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -1,6 +1,9 @@ import type { QueryClient } from "react-query"; import { API } from "api/api"; -import type { CreateOrganizationRequest } from "api/typesGenerated"; +import type { + CreateOrganizationRequest, + UpdateOrganizationRequest, +} from "api/typesGenerated"; import { meKey, myOrganizationsKey } from "./users"; export const createOrganization = (queryClient: QueryClient) => { @@ -15,6 +18,23 @@ export const createOrganization = (queryClient: QueryClient) => { }; }; +interface UpdateOrganizationVariables { + orgId: string; + req: UpdateOrganizationRequest; +} + +export const updateOrganization = (queryClient: QueryClient) => { + return { + mutationFn: (variables: UpdateOrganizationVariables) => + API.updateOrganization(variables.orgId, variables.req), + + onSuccess: async () => { + await queryClient.invalidateQueries(meKey); + await queryClient.invalidateQueries(myOrganizationsKey); + }, + }; +}; + export const deleteOrganization = (queryClient: QueryClient) => { return { mutationFn: (orgId: string) => API.deleteOrganization(orgId), diff --git a/site/src/components/FormFooter/FormFooter.tsx b/site/src/components/FormFooter/FormFooter.tsx index 4c672cf8d8ee9..394268be48efe 100644 --- a/site/src/components/FormFooter/FormFooter.tsx +++ b/site/src/components/FormFooter/FormFooter.tsx @@ -14,7 +14,7 @@ export interface FormFooterStyles { } export interface FormFooterProps { - onCancel: () => void; + onCancel?: () => void; isLoading: boolean; styles?: FormFooterStyles; submitLabel?: string; @@ -45,15 +45,17 @@ export const FormFooter: FC = ({ > {submitLabel} - + {onCancel && ( + + )} {extraActions} ); diff --git a/site/src/components/Sidebar/Sidebar.tsx b/site/src/components/Sidebar/Sidebar.tsx index a89170cffd4d4..c9e49a4f4e8c9 100644 --- a/site/src/components/Sidebar/Sidebar.tsx +++ b/site/src/components/Sidebar/Sidebar.tsx @@ -77,6 +77,29 @@ export const SidebarNavItem: FC = ({ ); }; +interface SidebarNavSubItemProps { + children?: ReactNode; + href: string; +} + +export const SidebarNavSubItem: FC = ({ + children, + href, +}) => { + const link = useClassName(classNames.subLink, []); + const activeLink = useClassName(classNames.activeSubLink, []); + + return ( + cx([link, isActive && activeLink])} + > + {children} + + ); +}; + const styles = { sidebar: { width: 245, @@ -118,22 +141,35 @@ const classNames = { &:hover { background-color: ${theme.palette.action.hover}; } + + border-left: 3px solid transparent; `, activeLink: (css, theme) => css` - background-color: ${theme.palette.action.hover}; - - &:before { - content: ""; - display: block; - width: 3px; - height: 100%; - position: absolute; - left: 0; - top: 0; - background-color: ${theme.palette.primary.main}; - border-top-left-radius: 8px; - border-bottom-left-radius: 8px; + border-left-color: ${theme.palette.primary.main}; + `, + + subLink: (css, theme) => css` + color: inherit; + text-decoration: none; + + display: block; + font-size: 13px; + margin-left: 16px; + padding: 8px; + border-radius: 4px; + transition: background-color 0.15s ease-in-out; + margin-bottom: 1px; + position: relative; + border-left: 3px solid transparent; + + &:hover { + background-color: ${theme.palette.action.hover}; } `, + + activeSubLink: (css, theme) => css` + border-left-color: ${theme.palette.primary.main}; + font-weight: 500; + `, } satisfies Record; diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index 8a0b473398a70..cf1b3b842c4e3 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -12,7 +12,7 @@ export const Navbar: FC = () => { const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - const { appearance } = useDashboard(); + const { appearance, experiments } = useDashboard(); const { user: me, permissions, signOut } = useAuthenticated(); const featureVisibility = useFeatureVisibility(); const canViewAuditLog = @@ -29,10 +29,11 @@ export const Navbar: FC = () => { buildInfo={buildInfoQuery.data} supportLinks={appearance.support_links} onSignOut={signOut} - canViewAuditLog={canViewAuditLog} canViewDeployment={canViewDeployment} + canViewOrganizations={experiments.includes("multi-organization")} canViewAllUsers={canViewAllUsers} canViewHealth={canViewHealth} + canViewAuditLog={canViewAuditLog} proxyContextValue={proxyContextValue} /> ); diff --git a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx index a6541ea688486..02b40065905dc 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx @@ -28,10 +28,11 @@ describe("NavbarView", () => { proxyContextValue={proxyContextValue} user={MockUser} onSignOut={noop} - canViewAuditLog canViewDeployment + canViewOrganizations canViewAllUsers canViewHealth + canViewAuditLog />, ); const workspacesLink = await screen.findByText(navLanguage.workspaces); @@ -44,10 +45,11 @@ describe("NavbarView", () => { proxyContextValue={proxyContextValue} user={MockUser} onSignOut={noop} - canViewAuditLog canViewDeployment + canViewOrganizations canViewAllUsers canViewHealth + canViewAuditLog />, ); const templatesLink = await screen.findByText(navLanguage.templates); @@ -60,10 +62,11 @@ describe("NavbarView", () => { proxyContextValue={proxyContextValue} user={MockUser} onSignOut={noop} - canViewAuditLog canViewDeployment + canViewOrganizations canViewAllUsers canViewHealth + canViewAuditLog />, ); const deploymentMenu = await screen.findByText("Deployment"); @@ -78,10 +81,11 @@ describe("NavbarView", () => { proxyContextValue={proxyContextValue} user={MockUser} onSignOut={noop} - canViewAuditLog canViewDeployment + canViewOrganizations canViewAllUsers canViewHealth + canViewAuditLog />, ); const deploymentMenu = await screen.findByText("Deployment"); @@ -96,10 +100,11 @@ describe("NavbarView", () => { proxyContextValue={proxyContextValue} user={MockUser} onSignOut={noop} - canViewAuditLog canViewDeployment + canViewOrganizations canViewAllUsers canViewHealth + canViewAuditLog />, ); const deploymentMenu = await screen.findByText("Deployment"); diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 6e9cea551b24b..77733bc63e920 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -19,9 +19,10 @@ export interface NavbarViewProps { buildInfo?: TypesGen.BuildInfoResponse; supportLinks?: readonly TypesGen.LinkConfig[]; onSignOut: () => void; - canViewAuditLog: boolean; canViewDeployment: boolean; + canViewOrganizations: boolean; canViewAllUsers: boolean; + canViewAuditLog: boolean; canViewHealth: boolean; proxyContextValue?: ProxyContextValue; } @@ -69,10 +70,11 @@ export const NavbarView: FC = ({ buildInfo, supportLinks, onSignOut, - canViewAuditLog, canViewDeployment, + canViewOrganizations, canViewAllUsers, canViewHealth, + canViewAuditLog, proxyContextValue, }) => { const theme = useTheme(); @@ -134,7 +136,7 @@ export const NavbarView: FC = ({ = ({ onSignOut, }) => { const theme = useTheme(); - const organizationsQuery = useQuery({ - ...myOrganizations(), - enabled: Boolean(localStorage.getItem("enableMultiOrganizationUi")), - }); - const { organizationId, setOrganizationId } = useDashboard(); return ( @@ -71,9 +63,6 @@ export const UserDropdown: FC = ({ user={user} buildInfo={buildInfo} supportLinks={supportLinks} - organizations={organizationsQuery.data} - organizationId={organizationId} - setOrganizationId={setOrganizationId} onSignOut={onSignOut} /> diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index c0ad5111ea9ae..b8766698d4ca7 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -29,9 +29,6 @@ export const Language = { export interface UserDropdownContentProps { user: TypesGen.User; - organizations?: TypesGen.Organization[]; - organizationId?: string; - setOrganizationId?: (id: string) => void; buildInfo?: TypesGen.BuildInfoResponse; supportLinks?: readonly TypesGen.LinkConfig[]; onSignOut: () => void; @@ -39,9 +36,6 @@ export interface UserDropdownContentProps { export const UserDropdownContent: FC = ({ user, - organizations, - organizationId, - setOrganizationId, buildInfo, supportLinks, onSignOut, @@ -79,43 +73,6 @@ export const UserDropdownContent: FC = ({ - {organizations && ( - <> -
-
- My teams -
- {organizations.map((org) => ( - { - setOrganizationId?.(org.id); - popover.setIsOpen(false); - }} - > - {/* */} - - {org.name} - {organizationId === org.id && ( - Current - )} - - - ))} -
- - - )} - diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index 8370be000e9c1..bbc7f45288385 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -25,7 +25,7 @@ import { nameValidator, getFormHelpers, onChangeTrimmed, - templateDisplayNameValidator, + displayNameValidator, } from "utils/formUtils"; import { sortedDays, @@ -57,7 +57,7 @@ export interface CreateTemplateData { const validationSchema = Yup.object({ name: nameValidator("Name"), - display_name: templateDisplayNameValidator("Display name"), + display_name: displayNameValidator("Display name"), description: Yup.string().max( MAX_DESCRIPTION_CHAR_LIMIT, "Please enter a description that is less than or equal to 128 characters.", diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx new file mode 100644 index 0000000000000..fe7a577462cc3 --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx @@ -0,0 +1,74 @@ +import { createContext, type FC, Suspense, useContext } from "react"; +import { useQuery } from "react-query"; +import { Outlet, useParams } from "react-router-dom"; +import { myOrganizations } from "api/queries/users"; +import { Organization } from "api/typesGenerated"; +import { Loader } from "components/Loader/Loader"; +import { Margins } from "components/Margins/Margins"; +import { Stack } from "components/Stack/Stack"; +import { useAuthenticated } from "contexts/auth/RequireAuth"; +import { RequirePermission } from "contexts/auth/RequirePermission"; +import { Sidebar } from "./Sidebar"; +import { useDashboard } from "modules/dashboard/useDashboard"; +import NotFoundPage from "pages/404Page/404Page"; + +type OrganizationSettingsContextValue = { + currentOrganizationId: string; + organizations: Organization[]; +}; + +const OrganizationSettingsContext = createContext< + OrganizationSettingsContextValue | undefined +>(undefined); + +export const useOrganizationSettings = (): OrganizationSettingsContextValue => { + const context = useContext(OrganizationSettingsContext); + if (!context) { + throw new Error( + "useOrganizationSettings should be used inside of OrganizationSettingsLayout", + ); + } + return context; +}; + +export const OrganizationSettingsLayout: FC = () => { + const { permissions, organizationIds } = useAuthenticated(); + const { experiments } = useDashboard(); + const { organization } = useParams() as { organization: string }; + const organizationsQuery = useQuery(myOrganizations()); + + const multiOrgExperimentEnabled = experiments.includes("multi-organization"); + + if (!multiOrgExperimentEnabled) { + return ; + } + + return ( + + + + {organizationsQuery.data ? ( + org.name === organization, + )?.id ?? organizationIds[0], + organizations: organizationsQuery.data, + }} + > + +
+ }> + + +
+
+ ) : ( + + )} +
+
+
+ ); +}; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 68a7f3a86e04d..19c1127daf9ca 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -1,31 +1,135 @@ +import type { Interpolation, Theme } from "@emotion/react"; import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; +import { useFormik } from "formik"; import { type FC, useState } from "react"; -import { useMutation, useQuery, useQueryClient } from "react-query"; +import { useMutation, useQueryClient } from "react-query"; import { createOrganization, + updateOrganization, deleteOrganization, } from "api/queries/organizations"; -import { myOrganizations } from "api/queries/users"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Margins } from "components/Margins/Margins"; +import { useOrganizationSettings } from "./OrganizationSettingsLayout"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { + getFormHelpers, + nameValidator, + displayNameValidator, + onChangeTrimmed, +} from "utils/formUtils"; +import { UpdateOrganizationRequest } from "api/typesGenerated"; +import { + FormFields, + FormSection, + HorizontalForm, + FormFooter, +} from "components/Form/Form"; +import * as Yup from "yup"; + +const MAX_DESCRIPTION_CHAR_LIMIT = 128; +const MAX_DESCRIPTION_MESSAGE = + "Please enter a description that is no longer than 128 characters."; + +export const getValidationSchema = (): Yup.AnyObjectSchema => + Yup.object({ + name: nameValidator("Name"), + display_name: displayNameValidator("Display name"), + description: Yup.string().max( + MAX_DESCRIPTION_CHAR_LIMIT, + MAX_DESCRIPTION_MESSAGE, + ), + }); const OrganizationSettingsPage: FC = () => { const queryClient = useQueryClient(); const addOrganizationMutation = useMutation(createOrganization(queryClient)); + const updateOrganizationMutation = useMutation( + updateOrganization(queryClient), + ); const deleteOrganizationMutation = useMutation( deleteOrganization(queryClient), ); - const organizationsQuery = useQuery(myOrganizations()); - const [newOrgName, setNewOrgName] = useState(""); + + const { currentOrganizationId, organizations } = useOrganizationSettings(); + + const org = organizations.find((org) => org.id === currentOrganizationId)!; const error = addOrganizationMutation.error ?? deleteOrganizationMutation.error; + const form = useFormik({ + initialValues: { + name: org.name, + display_name: org.display_name, + description: org.description, + }, + validationSchema: getValidationSchema(), + onSubmit: (values) => + updateOrganizationMutation.mutateAsync({ orgId: org.id, req: values }), + }); + const getFieldHelpers = getFormHelpers(form, error); + + const [newOrgName, setNewOrgName] = useState(""); + return ( - + {Boolean(error) && } + + Organization settings + + + + + + + + + + + + + + {!org.is_default && ( + + )} + +
setNewOrgName(event.target.value)} @@ -33,19 +137,37 @@ const OrganizationSettingsPage: FC = () => { - - {organizationsQuery.data?.map((org) => ( -
- {org.name}{" "} - -
- ))}
); }; export default OrganizationSettingsPage; + +const styles = { + dangerButton: (theme) => ({ + "&.MuiButton-contained": { + backgroundColor: theme.roles.danger.fill.solid, + borderColor: theme.roles.danger.fill.outline, + + "&:not(.MuiLoadingButton-loading)": { + color: theme.roles.danger.fill.text, + }, + + "&:hover:not(:disabled)": { + backgroundColor: theme.roles.danger.hover.fill.solid, + borderColor: theme.roles.danger.hover.fill.outline, + }, + + "&.Mui-disabled": { + backgroundColor: theme.roles.danger.disabled.background, + borderColor: theme.roles.danger.disabled.outline, + + "&:not(.MuiLoadingButton-loading)": { + color: theme.roles.danger.disabled.fill.text, + }, + }, + }, + }), +} satisfies Record>; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx new file mode 100644 index 0000000000000..ad63cc5af75fe --- /dev/null +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx @@ -0,0 +1,37 @@ +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 "./OrganizationSettingsLayout"; + +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/OrganizationSettingsPage/Sidebar.tsx b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx index 04129a827fe8e..925520ef752f6 100644 --- a/site/src/pages/OrganizationSettingsPage/Sidebar.tsx +++ b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx @@ -1,21 +1,172 @@ +import { cx } from "@emotion/css"; import GeneralIcon from "@mui/icons-material/SettingsOutlined"; -import type { FC } from "react"; -import type { Template } from "api/typesGenerated"; -import { - Sidebar as BaseSidebar, - SidebarNavItem, -} from "components/Sidebar/Sidebar"; - -interface SidebarProps { - template: Template; -} +import type { ElementType, FC, ReactNode } from "react"; +import { Link, NavLink } 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 { type ClassName, useClassName } from "hooks/useClassName"; +import { useOrganizationSettings } from "./OrganizationSettingsLayout"; + +export const Sidebar: FC = () => { + const { currentOrganizationId, organizations } = useOrganizationSettings(); + + // maybe do something nice to scroll to the active org -export const Sidebar: FC = ({ template }) => { return ( - - General - + {organizations.map((organization) => ( + + ))} ); }; + +interface BloobProps { + organization: Organization; + active: boolean; +} + +function urlForSubpage(organizationName: string, subpage: string = ""): string { + return `/organizations/${organizationName}/${subpage}`; +} + +export const OrganizationBloob: FC = ({ organization, active }) => { + return ( + <> + + {organization.display_name} + + {active && ( + + + Organization settings + + + External authentication + + + Members + + + Groups + + + Metrics + + + Auditing + + + )} + + ); +}; + +interface SidebarNavItemProps { + children?: ReactNode; + icon: ElementType; + href: string; +} + +export const SidebarNavItem: FC = ({ + children, + href, + icon: Icon, +}) => { + const link = useClassName(classNames.link, []); + const activeLink = useClassName(classNames.activeLink, []); + + return ( + cx([link, isActive && activeLink])} + > + + + {children} + + + ); +}; + +interface SidebarNavSubItemProps { + children?: ReactNode; + href: string; +} + +export const SidebarNavSubItem: FC = ({ + children, + href, +}) => { + const link = useClassName(classNames.subLink, []); + const activeLink = useClassName(classNames.activeSubLink, []); + + return ( + cx([link, isActive && activeLink])} + > + {children} + + ); +}; + +const classNames = { + link: (css, theme) => css` + color: inherit; + display: block; + font-size: 14px; + text-decoration: none; + padding: 10px 12px 10px 16px; + border-radius: 4px; + transition: background-color 0.15s ease-in-out; + position: relative; + + &:hover { + background-color: ${theme.palette.action.hover}; + } + + border-left: 3px solid transparent; + `, + + activeLink: (css, theme) => css` + border-left-color: ${theme.palette.primary.main}; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + `, + + subLink: (css, theme) => css` + color: inherit; + text-decoration: none; + + display: block; + font-size: 13px; + margin-left: 35px; + padding: 4px 12px; + border-radius: 4px; + transition: background-color 0.15s ease-in-out; + margin-bottom: 1px; + position: relative; + + &:hover { + background-color: ${theme.palette.action.hover}; + } + `, + + activeSubLink: (css) => css` + font-weight: 500; + `, +} satisfies Record; diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 3e6cc138426ca..080cd9b57f777 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -3,7 +3,7 @@ import FormControlLabel from "@mui/material/FormControlLabel"; import FormHelperText from "@mui/material/FormHelperText"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; -import { type FormikContextType, type FormikTouched, useFormik } from "formik"; +import { type FormikTouched, useFormik } from "formik"; import type { FC } from "react"; import * as Yup from "yup"; import { @@ -27,7 +27,7 @@ import { import { getFormHelpers, nameValidator, - templateDisplayNameValidator, + displayNameValidator, onChangeTrimmed, iconValidator, } from "utils/formUtils"; @@ -39,7 +39,7 @@ const MAX_DESCRIPTION_MESSAGE = export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object({ name: nameValidator("Name"), - display_name: templateDisplayNameValidator("Display name"), + display_name: displayNameValidator("Display name"), description: Yup.string().max( MAX_DESCRIPTION_CHAR_LIMIT, MAX_DESCRIPTION_MESSAGE, @@ -76,26 +76,25 @@ export const TemplateSettingsForm: FC = ({ portSharingControlsEnabled, }) => { const validationSchema = getValidationSchema(); - const form: FormikContextType = - useFormik({ - initialValues: { - name: template.name, - display_name: template.display_name, - description: template.description, - icon: template.icon, - allow_user_cancel_workspace_jobs: - template.allow_user_cancel_workspace_jobs, - update_workspace_last_used_at: false, - update_workspace_dormant_at: false, - require_active_version: template.require_active_version, - deprecation_message: template.deprecation_message, - disable_everyone_group_access: false, - max_port_share_level: template.max_port_share_level, - }, - validationSchema, - onSubmit, - initialTouched, - }); + const form = useFormik({ + initialValues: { + name: template.name, + display_name: template.display_name, + description: template.description, + icon: template.icon, + allow_user_cancel_workspace_jobs: + template.allow_user_cancel_workspace_jobs, + update_workspace_last_used_at: false, + update_workspace_dormant_at: false, + require_active_version: template.require_active_version, + deprecation_message: template.deprecation_message, + disable_everyone_group_access: false, + max_port_share_level: template.max_port_share_level, + }, + validationSchema, + onSubmit, + initialTouched, + }); const getFieldHelpers = getFormHelpers(form, error); return ( diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx index 3a299e37b20aa..db44a26f456e0 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx @@ -58,23 +58,6 @@ export const AccountPage: FC = () => { error={groupsQuery.error} /> )} - - {multiOrgExperimentEnabled && ( -
Danger: enabling will break things in the UI. - } - > - - {multiOrgUiEnabled ? : } - - -
- )} ); }; diff --git a/site/src/router.tsx b/site/src/router.tsx index e5d1a2566320c..e2685c29f69c8 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -13,6 +13,7 @@ import AuditPage from "./pages/AuditPage/AuditPage"; import { DeploySettingsLayout } from "./pages/DeploySettingsPage/DeploySettingsLayout"; import { HealthLayout } from "./pages/HealthPage/HealthLayout"; import LoginPage from "./pages/LoginPage/LoginPage"; +import { OrganizationSettingsLayout } from "./pages/OrganizationSettingsPage/OrganizationSettingsLayout"; import { SetupPage } from "./pages/SetupPage/SetupPage"; import { TemplateLayout } from "./pages/TemplatePage/TemplateLayout"; import { TemplateSettingsLayout } from "./pages/TemplateSettingsPage/TemplateSettingsLayout"; @@ -223,6 +224,10 @@ const AddNewLicensePage = lazy( const OrganizationSettingsPage = lazy( () => import("./pages/OrganizationSettingsPage/OrganizationSettingsPage"), ); +const OrganizationSettingsPlaceholder = lazy( + () => + import("./pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder"), +); const TemplateEmbedPage = lazy( () => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"), ); @@ -328,7 +333,32 @@ export const router = createBrowserRouter( } /> - } /> + } + > + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + }> } /> diff --git a/site/src/utils/formUtils.ts b/site/src/utils/formUtils.ts index c48eeb301383f..846414eecd95b 100644 --- a/site/src/utils/formUtils.ts +++ b/site/src/utils/formUtils.ts @@ -18,7 +18,7 @@ const Language = { nameTooLong: (name: string, len: number): string => { return `${name} cannot be longer than ${len} characters`; }, - templateDisplayNameInvalidChars: (name: string): string => { + displayNameInvalidChars: (name: string): string => { return `${name} must start and end with non-whitespace character`; }, }; @@ -114,9 +114,9 @@ export const onChangeTrimmed = // REMARK: Keep these consts in sync with coderd/httpapi/httpapi.go const maxLenName = 32; -const templateDisplayNameMaxLength = 64; +const displayNameMaxLength = 64; const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/; -const templateDisplayNameRE = /^[^\s](.*[^\s])?$/; +const displayNameRE = /^[^\s](.*[^\s])?$/; // REMARK: see #1756 for name/username semantics export const nameValidator = (name: string): Yup.StringSchema => @@ -125,17 +125,12 @@ export const nameValidator = (name: string): Yup.StringSchema => .matches(usernameRE, Language.nameInvalidChars(name)) .max(maxLenName, Language.nameTooLong(name, maxLenName)); -export const templateDisplayNameValidator = ( - displayName: string, -): Yup.StringSchema => +export const displayNameValidator = (displayName: string): Yup.StringSchema => Yup.string() - .matches( - templateDisplayNameRE, - Language.templateDisplayNameInvalidChars(displayName), - ) + .matches(displayNameRE, Language.displayNameInvalidChars(displayName)) .max( - templateDisplayNameMaxLength, - Language.nameTooLong(displayName, templateDisplayNameMaxLength), + displayNameMaxLength, + Language.nameTooLong(displayName, displayNameMaxLength), ) .optional(); From 17a890d54813fc3597f92bf451eabf76b8882b5d Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 7 Jun 2024 21:14:29 +0000 Subject: [PATCH 07/19] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clock/mock.go | 2 +- .../OrganizationSettingsLayout.tsx | 4 ++-- .../OrganizationSettingsPage.tsx | 18 ++++++++-------- .../OrganizationSettingsPlaceholder.tsx | 2 +- .../OrganizationSettingsPage/Sidebar.tsx | 2 +- .../AccountPage/AccountPage.tsx | 21 ++----------------- 6 files changed, 16 insertions(+), 33 deletions(-) diff --git a/clock/mock.go b/clock/mock.go index 55c8cdcaa3277..6b2b09b530b69 100644 --- a/clock/mock.go +++ b/clock/mock.go @@ -532,7 +532,7 @@ func (t *Trap) Close() { close(t.done) } -var ErrTrapClosed = errors.New("trap closed") +var ErrTrapClosed = xerrors.New("trap closed") func (t *Trap) Wait(ctx context.Context) (*Call, error) { select { diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx index fe7a577462cc3..ae278b053428a 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx @@ -2,15 +2,15 @@ import { createContext, type FC, Suspense, useContext } from "react"; import { useQuery } from "react-query"; import { Outlet, useParams } from "react-router-dom"; import { myOrganizations } from "api/queries/users"; -import { Organization } from "api/typesGenerated"; +import type { Organization } from "api/typesGenerated"; import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; import { Stack } from "components/Stack/Stack"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { RequirePermission } from "contexts/auth/RequirePermission"; -import { Sidebar } from "./Sidebar"; import { useDashboard } from "modules/dashboard/useDashboard"; import NotFoundPage from "pages/404Page/404Page"; +import { Sidebar } from "./Sidebar"; type OrganizationSettingsContextValue = { currentOrganizationId: string; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 19c1127daf9ca..5a4963527857c 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -4,14 +4,21 @@ import TextField from "@mui/material/TextField"; import { useFormik } from "formik"; import { type FC, useState } from "react"; import { useMutation, useQueryClient } from "react-query"; +import * as Yup from "yup"; import { createOrganization, updateOrganization, deleteOrganization, } from "api/queries/organizations"; +import type { UpdateOrganizationRequest } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { + FormFields, + FormSection, + HorizontalForm, + FormFooter, +} from "components/Form/Form"; import { Margins } from "components/Margins/Margins"; -import { useOrganizationSettings } from "./OrganizationSettingsLayout"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { getFormHelpers, @@ -19,14 +26,7 @@ import { displayNameValidator, onChangeTrimmed, } from "utils/formUtils"; -import { UpdateOrganizationRequest } from "api/typesGenerated"; -import { - FormFields, - FormSection, - HorizontalForm, - FormFooter, -} from "components/Form/Form"; -import * as Yup from "yup"; +import { useOrganizationSettings } from "./OrganizationSettingsLayout"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; const MAX_DESCRIPTION_MESSAGE = diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx index ad63cc5af75fe..dce7a33715717 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx @@ -1,4 +1,4 @@ -import { type FC } from "react"; +import type { FC } from "react"; import { useMutation, useQueryClient } from "react-query"; import { createOrganization, diff --git a/site/src/pages/OrganizationSettingsPage/Sidebar.tsx b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx index 925520ef752f6..e5fb4028dce2a 100644 --- a/site/src/pages/OrganizationSettingsPage/Sidebar.tsx +++ b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx @@ -1,7 +1,7 @@ import { cx } from "@emotion/css"; import GeneralIcon from "@mui/icons-material/SettingsOutlined"; import type { ElementType, FC, ReactNode } from "react"; -import { Link, NavLink } from "react-router-dom"; +import { NavLink } from "react-router-dom"; import type { Organization } from "api/typesGenerated"; import { Sidebar as BaseSidebar } from "components/Sidebar/Sidebar"; import { Stack } from "components/Stack/Stack"; diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx index db44a26f456e0..55bebfb1b53ec 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx @@ -1,8 +1,6 @@ -import Button from "@mui/material/Button"; -import { type FC, useEffect, useState } from "react"; +import type { FC } from "react"; import { useQuery } from "react-query"; import { groupsForUser } from "api/queries/groups"; -import { DisabledBadge, EnabledBadge } from "components/Badges/Badges"; import { Stack } from "components/Stack/Stack"; import { useAuthContext } from "contexts/auth/AuthProvider"; import { useAuthenticated } from "contexts/auth/RequireAuth"; @@ -15,7 +13,7 @@ export const AccountPage: FC = () => { const { permissions, user: me } = useAuthenticated(); const { updateProfile, updateProfileError, isUpdatingProfile } = useAuthContext(); - const { entitlements, experiments, organizationId } = useDashboard(); + const { entitlements, organizationId } = useDashboard(); const hasGroupsFeature = entitlements.features.user_role_management.enabled; const groupsQuery = useQuery({ @@ -23,21 +21,6 @@ export const AccountPage: FC = () => { enabled: hasGroupsFeature, }); - const multiOrgExperimentEnabled = experiments.includes("multi-organization"); - const [multiOrgUiEnabled, setMultiOrgUiEnabled] = useState( - () => - multiOrgExperimentEnabled && - Boolean(localStorage.getItem("enableMultiOrganizationUi")), - ); - - useEffect(() => { - if (multiOrgUiEnabled) { - localStorage.setItem("enableMultiOrganizationUi", "true"); - } else { - localStorage.removeItem("enableMultiOrganizationUi"); - } - }, [multiOrgUiEnabled]); - return (
From 050d553c5483f9dde05a5e90ac9fe69f3a6b66fd Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 10 Jun 2024 21:26:51 +0000 Subject: [PATCH 08/19] wibbles --- .../pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 5a4963527857c..cbe360ef9e823 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -87,7 +87,7 @@ const OrganizationSettingsPage: FC = () => { > Date: Mon, 10 Jun 2024 21:33:02 +0000 Subject: [PATCH 09/19] =?UTF-8?q?=F0=9F=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clock/mock.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clock/mock.go b/clock/mock.go index 9a90aea728f9a..97e7a16874851 100644 --- a/clock/mock.go +++ b/clock/mock.go @@ -2,12 +2,13 @@ package clock import ( "context" - "errors" "fmt" "slices" "sync" "testing" "time" + + "golang.org/x/xerrors" ) // Mock is the testing implementation of Clock. It tracks a time that monotonically increases From b0712318ffde12635099d057d931dad788c94476 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 11 Jun 2024 17:44:14 +0000 Subject: [PATCH 10/19] formik strikes again --- .../pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index cbe360ef9e823..f202f2d3d8fa9 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -68,6 +68,7 @@ const OrganizationSettingsPage: FC = () => { validationSchema: getValidationSchema(), onSubmit: (values) => updateOrganizationMutation.mutateAsync({ orgId: org.id, req: values }), + enableReinitialize: true, }); const getFieldHelpers = getFormHelpers(form, error); From f8ccd9652fd4b759629b714c8f6f3f170b30c02c Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 11 Jun 2024 17:51:52 +0000 Subject: [PATCH 11/19] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/api/queries/organizations.ts | 1 - site/src/components/Sidebar/Sidebar.tsx | 62 ++++++------------------- 2 files changed, 13 insertions(+), 50 deletions(-) diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 836a452431d83..e9526e74ca3f2 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -29,7 +29,6 @@ export const updateOrganization = (queryClient: QueryClient) => { API.updateOrganization(variables.orgId, variables.req), onSuccess: async () => { - await queryClient.invalidateQueries(meKey); await queryClient.invalidateQueries(myOrganizationsKey); }, }; diff --git a/site/src/components/Sidebar/Sidebar.tsx b/site/src/components/Sidebar/Sidebar.tsx index c9e49a4f4e8c9..a89170cffd4d4 100644 --- a/site/src/components/Sidebar/Sidebar.tsx +++ b/site/src/components/Sidebar/Sidebar.tsx @@ -77,29 +77,6 @@ export const SidebarNavItem: FC = ({ ); }; -interface SidebarNavSubItemProps { - children?: ReactNode; - href: string; -} - -export const SidebarNavSubItem: FC = ({ - children, - href, -}) => { - const link = useClassName(classNames.subLink, []); - const activeLink = useClassName(classNames.activeSubLink, []); - - return ( - cx([link, isActive && activeLink])} - > - {children} - - ); -}; - const styles = { sidebar: { width: 245, @@ -141,35 +118,22 @@ const classNames = { &:hover { background-color: ${theme.palette.action.hover}; } - - border-left: 3px solid transparent; `, activeLink: (css, theme) => css` - border-left-color: ${theme.palette.primary.main}; - `, - - subLink: (css, theme) => css` - color: inherit; - text-decoration: none; - - display: block; - font-size: 13px; - margin-left: 16px; - padding: 8px; - border-radius: 4px; - transition: background-color 0.15s ease-in-out; - margin-bottom: 1px; - position: relative; - border-left: 3px solid transparent; - - &:hover { - background-color: ${theme.palette.action.hover}; + background-color: ${theme.palette.action.hover}; + + &:before { + content: ""; + display: block; + width: 3px; + height: 100%; + position: absolute; + left: 0; + top: 0; + background-color: ${theme.palette.primary.main}; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; } `, - - activeSubLink: (css, theme) => css` - border-left-color: ${theme.palette.primary.main}; - font-weight: 500; - `, } satisfies Record; From 0a2bfea9536e0c898c5fcd38580d2d2dab40b33a Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 13 Jun 2024 15:46:04 +0000 Subject: [PATCH 12/19] add support for icon --- .../OrganizationSettingsPage.tsx | 9 +++++ .../OrganizationSettingsPage/Sidebar.tsx | 35 +++++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index f202f2d3d8fa9..543d00069c781 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -27,6 +27,7 @@ import { onChangeTrimmed, } from "utils/formUtils"; import { useOrganizationSettings } from "./OrganizationSettingsLayout"; +import { IconField } from "components/IconField/IconField"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; const MAX_DESCRIPTION_MESSAGE = @@ -64,6 +65,7 @@ const OrganizationSettingsPage: FC = () => { name: org.name, display_name: org.display_name, description: org.description, + icon: org.icon, }, validationSchema: getValidationSchema(), onSubmit: (values) => @@ -113,6 +115,13 @@ const OrganizationSettingsPage: FC = () => { label="Description" rows={2} /> + form.setFieldValue("icon", value)} + /> diff --git a/site/src/pages/OrganizationSettingsPage/Sidebar.tsx b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx index e5fb4028dce2a..c18260658a4d5 100644 --- a/site/src/pages/OrganizationSettingsPage/Sidebar.tsx +++ b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx @@ -1,12 +1,12 @@ import { cx } from "@emotion/css"; -import GeneralIcon from "@mui/icons-material/SettingsOutlined"; -import type { ElementType, FC, ReactNode } from "react"; -import { NavLink } from "react-router-dom"; +import type { FC, ReactNode } from "react"; +import { Link, NavLink } 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 { type ClassName, useClassName } from "hooks/useClassName"; import { useOrganizationSettings } from "./OrganizationSettingsLayout"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; export const Sidebar: FC = () => { const { currentOrganizationId, organizations } = useOrganizationSettings(); @@ -39,8 +39,16 @@ export const OrganizationBloob: FC = ({ organization, active }) => { return ( <> + } > {organization.display_name} @@ -75,29 +83,28 @@ export const OrganizationBloob: FC = ({ organization, active }) => { }; interface SidebarNavItemProps { + active?: boolean; children?: ReactNode; - icon: ElementType; + icon: ReactNode; href: string; } export const SidebarNavItem: FC = ({ + active, children, href, - icon: Icon, + icon, }) => { const link = useClassName(classNames.link, []); const activeLink = useClassName(classNames.activeLink, []); return ( - cx([link, isActive && activeLink])} - > + - + {icon} {children} - + ); }; @@ -154,7 +161,7 @@ const classNames = { display: block; font-size: 13px; - margin-left: 35px; + margin-left: 42px; padding: 4px 12px; border-radius: 4px; transition: background-color 0.15s ease-in-out; @@ -167,6 +174,6 @@ const classNames = { `, activeSubLink: (css) => css` - font-weight: 500; + font-weight: 600; `, } satisfies Record; From ed5725424307098ce15027cbd6e393b089ddda7e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 13 Jun 2024 15:48:28 +0000 Subject: [PATCH 13/19] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OrganizationSettingsPage.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 543d00069c781..cd22203f59fe4 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -28,6 +28,7 @@ import { } from "utils/formUtils"; import { useOrganizationSettings } from "./OrganizationSettingsLayout"; import { IconField } from "components/IconField/IconField"; +import { Stack } from "components/Stack/Stack"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; const MAX_DESCRIPTION_MESSAGE = @@ -139,16 +140,17 @@ const OrganizationSettingsPage: FC = () => { )} -
- setNewOrgName(event.target.value)} - /> - + + setNewOrgName(event.target.value)} + /> + + ); }; From d07a319546c9c5a4416d6bf5997093ea2b5655a0 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 13 Jun 2024 15:50:38 +0000 Subject: [PATCH 14/19] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OrganizationSettingsPage/OrganizationSettingsPage.tsx | 4 ++-- site/src/pages/OrganizationSettingsPage/Sidebar.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index cd22203f59fe4..b15fa8c7d7a18 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -18,8 +18,10 @@ import { HorizontalForm, FormFooter, } from "components/Form/Form"; +import { IconField } from "components/IconField/IconField"; import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; import { getFormHelpers, nameValidator, @@ -27,8 +29,6 @@ import { onChangeTrimmed, } from "utils/formUtils"; import { useOrganizationSettings } from "./OrganizationSettingsLayout"; -import { IconField } from "components/IconField/IconField"; -import { Stack } from "components/Stack/Stack"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; const MAX_DESCRIPTION_MESSAGE = diff --git a/site/src/pages/OrganizationSettingsPage/Sidebar.tsx b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx index c18260658a4d5..1c800e1f2f861 100644 --- a/site/src/pages/OrganizationSettingsPage/Sidebar.tsx +++ b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx @@ -4,9 +4,9 @@ import { Link, NavLink } 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 { useOrganizationSettings } from "./OrganizationSettingsLayout"; -import { UserAvatar } from "components/UserAvatar/UserAvatar"; export const Sidebar: FC = () => { const { currentOrganizationId, organizations } = useOrganizationSettings(); From c2263f1fc1e13284685963687669a871b078712a Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 13 Jun 2024 16:13:52 +0000 Subject: [PATCH 15/19] fix footer stories --- .../components/FormFooter/FormFooter.stories.tsx | 12 ++++++++++-- .../CreateTemplateForm.stories.tsx | 2 ++ .../CreateWorkspacePageView.stories.tsx | 2 ++ .../GroupsPage/SettingsGroupPageView.stories.tsx | 13 +++++++------ .../TemplateSettingsPageView.stories.tsx | 2 ++ .../TemplateVariablesPageView.stories.tsx | 4 ++++ .../WorkspaceParametersPage.stories.tsx | 2 ++ .../WorkspaceScheduleForm.stories.tsx | 1 + .../WorkspaceSettingsPageView.stories.tsx | 2 ++ 9 files changed, 32 insertions(+), 8 deletions(-) diff --git a/site/src/components/FormFooter/FormFooter.stories.tsx b/site/src/components/FormFooter/FormFooter.stories.tsx index 41d44250d04e1..20af1c5b437e4 100644 --- a/site/src/components/FormFooter/FormFooter.stories.tsx +++ b/site/src/components/FormFooter/FormFooter.stories.tsx @@ -1,23 +1,31 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { FormFooter } from "./FormFooter"; const meta: Meta = { title: "components/FormFooter", component: FormFooter, + args: { + isLoading: false, + onCancel: action("onCancel"), + }, }; export default meta; type Story = StoryObj; export const Ready: Story = { + args: {}, +}; + +export const NoCancel: Story = { args: { - isLoading: false, + onCancel: undefined, }, }; export const Custom: Story = { args: { - isLoading: false, submitLabel: "Create", }, }; diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx index e79da49a5337e..893de4d6bd688 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx @@ -1,3 +1,4 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { MockTemplate, @@ -15,6 +16,7 @@ const meta: Meta = { component: CreateTemplateForm, args: { isSubmitting: false, + onCancel: action("onCancel"), }, }; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 537c0280ba03d..a47d4b7b4c460 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -1,3 +1,4 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { chromatic } from "testHelpers/chromatic"; import { @@ -26,6 +27,7 @@ const meta: Meta = { permissions: { createWorkspaceForUser: true, }, + onCancel: action("onCancel"), }, }; diff --git a/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx b/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx index 48463eb1fc0a2..c8c93cb17a54d 100644 --- a/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx +++ b/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx @@ -1,20 +1,21 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MockGroup } from "testHelpers/entities"; import { SettingsGroupPageView } from "./SettingsGroupPageView"; +import { action } from "@storybook/addon-actions/*"; const meta: Meta = { title: "pages/GroupsPage/SettingsGroupPageView", component: SettingsGroupPageView, -}; - -export default meta; -type Story = StoryObj; - -const Example: Story = { args: { + onCancel: action("onCancel"), group: MockGroup, isLoading: false, }, }; +export default meta; +type Story = StoryObj; + +const Example: Story = {}; + export { Example as SettingsGroupPageView }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx index 1d63e8ade1cc0..5b3078af46bb6 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx @@ -1,3 +1,4 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { mockApiError, MockTemplate } from "testHelpers/entities"; import { TemplateSettingsPageView } from "./TemplateSettingsPageView"; @@ -9,6 +10,7 @@ const meta: Meta = { template: MockTemplate, accessControlEnabled: true, advancedSchedulingEnabled: true, + onCancel: action("onCancel"), }, }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx index ee03b8c3f3435..7cf1ba07a2ef6 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx @@ -1,3 +1,4 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { mockApiError, @@ -13,6 +14,9 @@ import { TemplateVariablesPageView } from "./TemplateVariablesPageView"; const meta: Meta = { title: "pages/TemplateSettingsPage/TemplateVariablesPageView", component: TemplateVariablesPageView, + args: { + onCancel: action("onCancel"), + }, }; export default meta; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx index 0314fa177ace0..a7e29c61dcec9 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx @@ -1,3 +1,4 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { MockWorkspaceBuildParameter1, @@ -19,6 +20,7 @@ const meta: Meta = { isSubmitting: false, workspace: MockWorkspace, canChangeVersions: true, + onCancel: action("onCancel"), data: { buildParameters: [ diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx index a67f17bb07c68..07f2583d453e4 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx @@ -10,6 +10,7 @@ import { import { emptyTTL } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"; import { MockTemplate, mockApiError } from "testHelpers/entities"; import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm"; +import { action } from "@storybook/addon-actions/*"; dayjs.extend(advancedFormat); dayjs.extend(utc); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx index b45281c0f4a9b..fff7f647a4ce6 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx @@ -1,3 +1,4 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { MockWorkspace } from "testHelpers/entities"; import { WorkspaceSettingsPageView } from "./WorkspaceSettingsPageView"; @@ -8,6 +9,7 @@ const meta: Meta = { args: { error: undefined, workspace: MockWorkspace, + onCancel: action("onCancel"), }, }; From 1c397bc75419f4c374418a9542af20511c2293a8 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 13 Jun 2024 16:17:14 +0000 Subject: [PATCH 16/19] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx | 2 +- .../WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx b/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx index c8c93cb17a54d..c715c82d74110 100644 --- a/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx +++ b/site/src/pages/GroupsPage/SettingsGroupPageView.stories.tsx @@ -1,7 +1,7 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { MockGroup } from "testHelpers/entities"; import { SettingsGroupPageView } from "./SettingsGroupPageView"; -import { action } from "@storybook/addon-actions/*"; const meta: Meta = { title: "pages/GroupsPage/SettingsGroupPageView", diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx index 07f2583d453e4..1a548db9bf88e 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx @@ -1,3 +1,4 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import dayjs from "dayjs"; import advancedFormat from "dayjs/plugin/advancedFormat"; @@ -10,7 +11,6 @@ import { import { emptyTTL } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"; import { MockTemplate, mockApiError } from "testHelpers/entities"; import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm"; -import { action } from "@storybook/addon-actions/*"; dayjs.extend(advancedFormat); dayjs.extend(utc); @@ -38,6 +38,7 @@ const meta: Meta = { component: WorkspaceScheduleForm, args: { template: mockTemplate, + onCancel: action("onCancel"), }, }; From 551191d58ba6d8c566134556a6005e03681221ba Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 14 Jun 2024 22:00:27 +0000 Subject: [PATCH 17/19] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/components/Margins/Margins.tsx | 9 +- .../OrganizationSettingsPage.tsx | 101 +++++++++--------- .../OrganizationSettingsPlaceholder.tsx | 2 +- .../OrganizationSettingsPage/Sidebar.tsx | 9 +- .../TemplateSettingsForm.tsx | 31 +++--- .../TemplateSettingsPage.test.tsx | 10 +- 6 files changed, 83 insertions(+), 79 deletions(-) diff --git a/site/src/components/Margins/Margins.tsx b/site/src/components/Margins/Margins.tsx index 49f55199f09b6..f5b120dded58d 100644 --- a/site/src/components/Margins/Margins.tsx +++ b/site/src/components/Margins/Margins.tsx @@ -15,12 +15,10 @@ const widthBySize: Record = { type MarginsProps = JSX.IntrinsicElements["div"] & { size?: Size; - verticalMargin?: string | number; }; export const Margins: FC = ({ size = "regular", - verticalMargin = 0, children, ...divProps }) => { @@ -29,12 +27,13 @@ export const Margins: FC = ({
{children}
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index b15fa8c7d7a18..35d9f000fab71 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -29,20 +29,19 @@ import { onChangeTrimmed, } from "utils/formUtils"; import { useOrganizationSettings } from "./OrganizationSettingsLayout"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; -const MAX_DESCRIPTION_MESSAGE = - "Please enter a description that is no longer than 128 characters."; - -export const getValidationSchema = (): Yup.AnyObjectSchema => - Yup.object({ - name: nameValidator("Name"), - display_name: displayNameValidator("Display name"), - description: Yup.string().max( - MAX_DESCRIPTION_CHAR_LIMIT, - MAX_DESCRIPTION_MESSAGE, - ), - }); +const MAX_DESCRIPTION_MESSAGE = `Please enter a description that is no longer than ${MAX_DESCRIPTION_CHAR_LIMIT} characters.`; + +export const validationSchema = Yup.object({ + name: nameValidator("Name"), + display_name: displayNameValidator("Display name"), + description: Yup.string().max( + MAX_DESCRIPTION_CHAR_LIMIT, + MAX_DESCRIPTION_MESSAGE, + ), +}); const OrganizationSettingsPage: FC = () => { const queryClient = useQueryClient(); @@ -68,9 +67,14 @@ const OrganizationSettingsPage: FC = () => { description: org.description, icon: org.icon, }, - validationSchema: getValidationSchema(), - onSubmit: (values) => - updateOrganizationMutation.mutateAsync({ orgId: org.id, req: values }), + validationSchema, + onSubmit: async (values) => { + await updateOrganizationMutation.mutateAsync({ + orgId: org.id, + req: values, + }); + displaySuccess("Organization settings updated."); + }, enableReinitialize: true, }); const getFieldHelpers = getFormHelpers(form, error); @@ -78,7 +82,7 @@ const OrganizationSettingsPage: FC = () => { const [newOrgName, setNewOrgName] = useState(""); return ( - + {Boolean(error) && } @@ -93,37 +97,38 @@ const OrganizationSettingsPage: FC = () => { title="General info" description="Change the name or description of the organization." > - - - - - form.setFieldValue("icon", value)} - /> - +
+ + + + + form.setFieldValue("icon", value)} + /> + +
@@ -148,7 +153,7 @@ const OrganizationSettingsPage: FC = () => {
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx index dce7a33715717..d0b3d95bc894c 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPlaceholder.tsx @@ -23,7 +23,7 @@ const OrganizationSettingsPage: FC = () => { addOrganizationMutation.error ?? deleteOrganizationMutation.error; return ( - + {Boolean(error) && }

Organization settings

diff --git a/site/src/pages/OrganizationSettingsPage/Sidebar.tsx b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx index 1c800e1f2f861..20b45d44de344 100644 --- a/site/src/pages/OrganizationSettingsPage/Sidebar.tsx +++ b/site/src/pages/OrganizationSettingsPage/Sidebar.tsx @@ -11,12 +11,12 @@ import { useOrganizationSettings } from "./OrganizationSettingsLayout"; export const Sidebar: FC = () => { const { currentOrganizationId, organizations } = useOrganizationSettings(); - // maybe do something nice to scroll to the active org + // TODO: Do something nice to scroll to the active org. return ( {organizations.map((organization) => ( - = ({ organization, active }) => { +export const OrganizationSettingsNavigation: FC = ({ + organization, + active, +}) => { return ( <> - Yup.object({ - name: nameValidator("Name"), - display_name: displayNameValidator("Display name"), - description: Yup.string().max( - MAX_DESCRIPTION_CHAR_LIMIT, - MAX_DESCRIPTION_MESSAGE, - ), - allow_user_cancel_workspace_jobs: Yup.boolean(), - icon: iconValidator, - require_active_version: Yup.boolean(), - deprecation_message: Yup.string(), - max_port_sharing_level: Yup.string().oneOf(WorkspaceAppSharingLevels), - }); +export const validationSchema = Yup.object({ + name: nameValidator("Name"), + display_name: displayNameValidator("Display name"), + description: Yup.string().max( + MAX_DESCRIPTION_CHAR_LIMIT, + MAX_DESCRIPTION_MESSAGE, + ), + allow_user_cancel_workspace_jobs: Yup.boolean(), + icon: iconValidator, + require_active_version: Yup.boolean(), + deprecation_message: Yup.string(), + max_port_sharing_level: Yup.string().oneOf(WorkspaceAppSharingLevels), +}); export interface TemplateSettingsForm { template: Template; @@ -75,7 +73,6 @@ export const TemplateSettingsForm: FC = ({ advancedSchedulingEnabled, portSharingControlsEnabled, }) => { - const validationSchema = getValidationSchema(); const form = useFormik({ initialValues: { name: template.name, diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index 716322f982288..7e7b44d8684d1 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -10,7 +10,7 @@ import { waitForLoaderToBeRemoved, } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; -import { getValidationSchema } from "./TemplateSettingsForm"; +import { validationSchema } from "./TemplateSettingsForm"; import { TemplateSettingsPage } from "./TemplateSettingsPage"; type FormValues = Required< @@ -116,9 +116,9 @@ describe("TemplateSettingsPage", () => { const values: UpdateTemplateMeta = { ...validFormValues, description: - "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port", + "The quick brown fox jumps over the lazy dog repeatedly, enjoying the weather of the bright, summer day in the lush, scenic park.", }; - const validate = () => getValidationSchema().validateSync(values); + const validate = () => validationSchema.validateSync(values); expect(validate).not.toThrowError(); }); @@ -126,9 +126,9 @@ describe("TemplateSettingsPage", () => { const values: UpdateTemplateMeta = { ...validFormValues, description: - "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port a", + "The quick brown fox jumps over the lazy dog multiple times, enjoying the warmth of the bright, sunny day in the lush, green park.", }; - const validate = () => getValidationSchema().validateSync(values); + const validate = () => validationSchema.validateSync(values); expect(validate).toThrowError(); }); From ab91af8e07ce72bab2b4abd1a8492b4ee260edd5 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 17 Jun 2024 16:44:04 +0000 Subject: [PATCH 18/19] show update errors and fix padding --- .../OrganizationSettingsPage/OrganizationSettingsPage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 35d9f000fab71..3dc714c876b36 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -58,7 +58,9 @@ const OrganizationSettingsPage: FC = () => { const org = organizations.find((org) => org.id === currentOrganizationId)!; const error = - addOrganizationMutation.error ?? deleteOrganizationMutation.error; + updateOrganizationMutation.error ?? + addOrganizationMutation.error ?? + deleteOrganizationMutation.error; const form = useFormik({ initialValues: { @@ -85,7 +87,7 @@ const OrganizationSettingsPage: FC = () => { {Boolean(error) && } - + Organization settings From e5bddc65b3e8cc4f3e5e98b24de99975b0f1abdf Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 17 Jun 2024 16:48:39 +0000 Subject: [PATCH 19/19] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index 3dc714c876b36..bc278b79c7e42 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -18,6 +18,7 @@ import { HorizontalForm, FormFooter, } from "components/Form/Form"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; import { IconField } from "components/IconField/IconField"; import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; @@ -29,7 +30,6 @@ import { onChangeTrimmed, } from "utils/formUtils"; import { useOrganizationSettings } from "./OrganizationSettingsLayout"; -import { displaySuccess } from "components/GlobalSnackbar/utils"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; const MAX_DESCRIPTION_MESSAGE = `Please enter a description that is no longer than ${MAX_DESCRIPTION_CHAR_LIMIT} characters.`; 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