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( pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy