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.`;