From e5d88c8a02755fe10e5eccea7c5383cef63d1941 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 29 Jan 2025 22:52:16 +0000 Subject: [PATCH 1/9] feat: add dropdown to select claim field value when sync field is set --- site/src/api/api.ts | 17 +++++++ site/src/api/queries/organizations.ts | 32 +++++++++++++ .../MultiSelectCombobox.tsx | 2 +- site/src/components/Select/Select.tsx | 13 ++--- .../management/OrganizationSidebarView.tsx | 9 +++- .../IdpOrgSyncPage/IdpOrgSyncPage.tsx | 14 +++++- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 47 +++++++++++++++---- 7 files changed, 117 insertions(+), 17 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 26491efb10565..cd21b5b063ac6 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -787,6 +787,23 @@ class ApiMethods { return response.data; }; + getIdpSyncClaimFieldValues = async (claimField: string) => { + const response = await this.axios.get( + `/api/v2/settings/idpsync/field-values?claimField=${claimField}`, + ); + return response.data; + }; + + getIdpSyncClaimFieldValuesByOrganization = async ( + organization: string, + claimField: string, + ) => { + const response = await this.axios.get( + `/api/v2/organizations/${organization}/settings/idpsync/field-values?claimField=${claimField}`, + ); + return response.data; + }; + getTemplate = async (templateId: string): Promise => { const response = await this.axios.get( `/api/v2/templates/${templateId}`, diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 0cc8168243c16..33ef19f0d2654 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -338,3 +338,35 @@ export const organizationsPermissions = ( }, }; }; + +export const getOrganizationIdpSyncClaimFieldValuesKey = ( + organization: string, + claimField: string, +) => [organization, claimField, "organizationIdpSyncClaimFieldValues"]; + +export const organizationIdpSyncClaimFieldValues = ( + organization: string, + claimField: string, +) => { + return { + queryKey: getOrganizationIdpSyncClaimFieldValuesKey( + organization, + claimField, + ), + queryFn: () => + API.getIdpSyncClaimFieldValuesByOrganization(organization, claimField), + }; +}; + +export const getIdpSyncClaimFieldValuesKey = (claimField: string) => [ + claimField, + "idpSyncClaimFieldValues", +]; + +export const idpSyncClaimFieldValues = (claimField: string) => { + return { + queryKey: getIdpSyncClaimFieldValuesKey(claimField), + queryFn: () => API.getIdpSyncClaimFieldValues(claimField), + enabled: !!claimField, + }; +}; diff --git a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx index 702be6a64d582..83f2aeed41cd4 100644 --- a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx +++ b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx @@ -572,7 +572,7 @@ export const MultiSelectCombobox = forwardRef< > - + diff --git a/site/src/components/Select/Select.tsx b/site/src/components/Select/Select.tsx index a0da638c907a2..ececcc2fc9950 100644 --- a/site/src/components/Select/Select.tsx +++ b/site/src/components/Select/Select.tsx @@ -20,17 +20,18 @@ export const SelectTrigger = React.forwardRef< span]:line-clamp-1", + `flex h-10 w-full font-medium items-center justify-between whitespace-nowrap rounded-md + border border-border border-solid bg-transparent px-3 py-2 text-sm shadow-sm + ring-offset-background text-content-secondary placeholder:text-content-secondary focus:outline-none, + focus:ring-2 focus:ring-content-link disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 + focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link`, className, )} {...props} > {children} - + )); @@ -65,7 +66,7 @@ export const SelectScrollDownButton = React.forwardRef< )} {...props} > - + )); SelectScrollDownButton.displayName = diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx index ef805861d1543..8d913edf87df3 100644 --- a/site/src/modules/management/OrganizationSidebarView.tsx +++ b/site/src/modules/management/OrganizationSidebarView.tsx @@ -18,7 +18,7 @@ import { SettingsSidebarNavItem, } from "components/Sidebar/Sidebar"; import type { Permissions } from "contexts/auth/permissions"; -import { ChevronDown, Plus } from "lucide-react"; +import { Check, ChevronDown, Plus } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { type FC, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -147,6 +147,13 @@ const OrganizationsSettingsNavigation: FC< {organization?.display_name || organization?.name} + {activeOrganization.name === organization.name && ( + + )} ))} diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx index d08b3aac4ab1a..923408fcc1575 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx @@ -3,6 +3,7 @@ import { organizationIdpSyncSettings, patchOrganizationSyncSettings, } from "api/queries/idpsync"; +import { idpSyncClaimFieldValues } from "api/queries/organizations"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { displayError } from "components/GlobalSnackbar/utils"; import { displaySuccess } from "components/GlobalSnackbar/utils"; @@ -11,7 +12,7 @@ import { Loader } from "components/Loader/Loader"; import { Paywall } from "components/Paywall/Paywall"; import { useDashboard } from "modules/dashboard/useDashboard"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; -import { type FC, useEffect } from "react"; +import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { docs } from "utils/docs"; @@ -29,6 +30,11 @@ export const IdpOrgSyncPage: FC = () => { isLoading, error, } = useQuery(organizationIdpSyncSettings(isIdpSyncEnabled)); + const [claimField, setClaimField] = useState(""); + + const { data: claimFieldValues } = useQuery( + idpSyncClaimFieldValues(claimField), + ); const patchOrganizationSyncSettingsMutation = useMutation( patchOrganizationSyncSettings(queryClient), @@ -49,6 +55,10 @@ export const IdpOrgSyncPage: FC = () => { return ; } + const handleSyncFieldChange = (value: string) => { + setClaimField(value); + }; + return ( <> @@ -94,6 +104,8 @@ export const IdpOrgSyncPage: FC = () => { ); } }} + onSyncFieldChange={handleSyncFieldChange} + claimFieldValues={claimFieldValues} error={error || patchOrganizationSyncSettingsMutation.error} /> diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index 7ed1b85e8c9dd..b51fd46e963a2 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -33,6 +33,13 @@ import { MultiSelectCombobox, type Option, } from "components/MultiSelectCombobox/MultiSelectCombobox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "components/Select/Select"; import { Spinner } from "components/Spinner/Spinner"; import { Switch } from "components/Switch/Switch"; import { useFormik } from "formik"; @@ -47,6 +54,8 @@ interface IdpSyncPageViewProps { organizationSyncSettings: OrganizationSyncSettings | undefined; organizations: readonly Organization[]; onSubmit: (data: OrganizationSyncSettings) => void; + onSyncFieldChange: (value: string) => void; + claimFieldValues: string[] | undefined; error?: unknown; } @@ -76,6 +85,8 @@ export const IdpOrgSyncPageView: FC = ({ organizationSyncSettings, organizations, onSubmit, + onSyncFieldChange, + claimFieldValues, error, }) => { const form = useFormik({ @@ -135,6 +146,7 @@ export const IdpOrgSyncPageView: FC = ({ value={form.values.field} onChange={(event) => { void form.setFieldValue("field", event.target.value); + onSyncFieldChange(event.target.value); }} /> + + + + + + +

No results found

+ + Enter custom value + + +
+ + {claimFieldValues.map((value) => ( + { + setIdpOrgName( + currentValue === idpOrgName + ? "" + : currentValue, + ); + setOpen(false); + }} + > + {value} + {idpOrgName === value && ( + + )} + + ))} + +
+
+
+ ) : ( Date: Tue, 4 Feb 2025 17:20:28 +0000 Subject: [PATCH 4/9] chore: use shadcn table --- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index 5d82cb08fa50d..a7b86f67f5ffb 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -1,9 +1,3 @@ -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; import type { Organization, OrganizationSyncSettings, @@ -48,6 +42,13 @@ import { } from "components/Popover/Popover"; import { Spinner } from "components/Spinner/Spinner"; import { Switch } from "components/Switch/Switch"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableRow, +} from "components/Table/Table"; import { useFormik } from "formik"; import { Check, ChevronDown, CornerDownLeft, Plus, Trash } from "lucide-react"; import { type FC, type KeyboardEventHandler, useId, useState } from "react"; @@ -218,7 +219,7 @@ export const IdpOrgSyncPageView: FC = ({ {form.errors.field}

)} -
+
{claimFieldValues ? ( - - - - - - - - - -

No results found

- - Enter custom value - - -
- - {claimFieldValues.map((value) => ( - { - setIdpOrgName( - currentValue === idpOrgName - ? "" - : currentValue, - ); - setOpen(false); - }} - > - {value} - {idpOrgName === value && ( - - )} - - ))} - -
-
-
-
+ { + setIdpOrgName(value); + setOpen(false); + }} + /> ) : ( Date: Tue, 4 Feb 2025 23:19:13 +0000 Subject: [PATCH 7/9] chore: PR comments --- .../IdpOrgSyncPage/IdpOrgSyncPage.tsx | 17 +++++++---------- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 6 +++--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx index 2897af32bbdb9..4d5b53e0f3ea2 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx @@ -30,17 +30,14 @@ export const IdpOrgSyncPage: FC = () => { data: orgSyncSettingsData, isLoading, error, - } = useQuery( - organizationIdpSyncSettings(isIdpSyncEnabled).queryKey, - organizationIdpSyncSettings(isIdpSyncEnabled).queryFn, - { - onSuccess: (data) => { - if (data?.field) { - setClaimField(data.field); - } - }, + } = useQuery({ + ...organizationIdpSyncSettings(isIdpSyncEnabled), + onSuccess: (data) => { + if (data?.field) { + setClaimField(data.field); + } }, - ); + }); const { data: claimFieldValues } = useQuery( idpSyncClaimFieldValues(claimField), diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index 284a1846a90e4..031234da0da25 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -132,13 +132,13 @@ export const IdpOrgSyncPageView: FC = ({ form.handleSubmit(); }; - const handleKeyDown: KeyboardEventHandler = (e) => { + const handleKeyDown: KeyboardEventHandler = (event) => { if ( - e.key === "Enter" && + event.key === "Enter" && inputValue && !claimFieldValues?.some((value) => value === inputValue.toLowerCase()) ) { - e.preventDefault(); + event.preventDefault(); setIdpOrgName(inputValue); setInputValue(""); setOpen(false); From 2d98f7e4350a3a70936c18414df8a16198c0908c Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 4 Feb 2025 23:36:43 +0000 Subject: [PATCH 8/9] fix: update test --- site/e2e/tests/deployment/idpOrgSync.spec.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/site/e2e/tests/deployment/idpOrgSync.spec.ts b/site/e2e/tests/deployment/idpOrgSync.spec.ts index a5162d4055658..e43820ab8fbce 100644 --- a/site/e2e/tests/deployment/idpOrgSync.spec.ts +++ b/site/e2e/tests/deployment/idpOrgSync.spec.ts @@ -150,6 +150,11 @@ test.describe("IdpOrgSyncPage", () => { waitUntil: "domcontentloaded", }); + const syncField = page.getByRole("textbox", { + name: "Organization sync field", + }); + await syncField.fill(""); + const idpOrgInput = page.getByLabel("IdP organization name"); const addButton = page.getByRole("button", { name: /Add IdP organization/i, @@ -157,7 +162,8 @@ test.describe("IdpOrgSyncPage", () => { await expect(addButton).toBeDisabled(); - await idpOrgInput.fill("new-idp-org"); + const idpOrgName = randomName(); + await idpOrgInput.fill(idpOrgName); // Select Coder organization from combobox const orgSelector = page.getByPlaceholder("Select organization"); @@ -177,10 +183,10 @@ test.describe("IdpOrgSyncPage", () => { await addButton.click(); // Verify new mapping appears in table - const newRow = page.getByTestId("idp-org-new-idp-org"); + const newRow = page.getByTestId(`idp-org-${idpOrgName}`); await expect(newRow).toBeVisible(); await expect( - newRow.getByRole("cell", { name: "new-idp-org" }), + newRow.getByRole("cell", { name: idpOrgName }), ).toBeVisible(); await expect(newRow.getByRole("cell", { name: orgName })).toBeVisible(); From cff6e0188c15594fa088fc813a0eb721c63ec1ac Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 5 Feb 2025 11:51:02 +0000 Subject: [PATCH 9/9] fix: format --- site/e2e/tests/deployment/idpOrgSync.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/e2e/tests/deployment/idpOrgSync.spec.ts b/site/e2e/tests/deployment/idpOrgSync.spec.ts index e43820ab8fbce..d77ddb1593fd3 100644 --- a/site/e2e/tests/deployment/idpOrgSync.spec.ts +++ b/site/e2e/tests/deployment/idpOrgSync.spec.ts @@ -185,9 +185,7 @@ test.describe("IdpOrgSyncPage", () => { // Verify new mapping appears in table const newRow = page.getByTestId(`idp-org-${idpOrgName}`); await expect(newRow).toBeVisible(); - await expect( - newRow.getByRole("cell", { name: idpOrgName }), - ).toBeVisible(); + await expect(newRow.getByRole("cell", { name: idpOrgName })).toBeVisible(); await expect(newRow.getByRole("cell", { name: orgName })).toBeVisible(); await expect(