From 7076c4e4ab530cae33fe686e1098b3128908d531 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 7 Feb 2025 21:31:02 +0000 Subject: [PATCH] feat: add combobox for selecting claim field value for group/role idp sync (#16459) contributes to coder/internal#330 This is a followup to coder/coder#16335 This adds a combobox that swaps with the input for the idp group and idp role names when the sync field returns claim values from the claim field values endpoint. If no claim field values are returned then the input remains instead of the combobox. Screenshot 2025-02-05 at 20 50 48 Screenshot 2025-02-05 at 20 51 00 --- site/src/components/Table/Table.tsx | 2 +- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 2 +- .../IdpSyncPage/IdpGroupSyncForm.tsx | 59 ++++++++++--- .../IdpSyncPage/IdpMappingTable.tsx | 88 +++++++++---------- .../IdpSyncPage/IdpRoleSyncForm.tsx | 59 ++++++++++--- .../IdpSyncPage/IdpSyncPage.tsx | 32 +++++-- .../IdpSyncPage/IdpSyncPageView.tsx | 6 ++ 7 files changed, 172 insertions(+), 76 deletions(-) diff --git a/site/src/components/Table/Table.tsx b/site/src/components/Table/Table.tsx index 8daf0b57f9..604fc3d4f4 100644 --- a/site/src/components/Table/Table.tsx +++ b/site/src/components/Table/Table.tsx @@ -68,7 +68,7 @@ export const TableRow = React.forwardRef< ref={ref} className={cn( "border-0 border-b border-solid border-border transition-colors", - "hover:bg-muted/50 data-[state=selected]:bg-muted", + "data-[state=selected]:bg-muted", className, )} {...props} diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index bdcc65b89a..5871cf98f2 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -367,7 +367,7 @@ const IdpMappingTable: FC = ({ isEmpty, children }) => { IdP organization Coder organization - + diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx index 9d63baf180..5340ec99dd 100644 --- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx +++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx @@ -1,11 +1,10 @@ -import TableCell from "@mui/material/TableCell"; -import TableRow from "@mui/material/TableRow"; import type { Group, GroupSyncSettings, Organization, } from "api/typesGenerated"; import { Button } from "components/Button/Button"; +import { Combobox } from "components/Combobox/Combobox"; import { HelpTooltip, HelpTooltipContent, @@ -22,6 +21,7 @@ import { } from "components/MultiSelectCombobox/MultiSelectCombobox"; import { Spinner } from "components/Spinner/Spinner"; import { Switch } from "components/Switch/Switch"; +import { TableCell, TableRow } from "components/Table/Table"; import { Tooltip, TooltipContent, @@ -30,7 +30,7 @@ import { } from "components/Tooltip/Tooltip"; import { useFormik } from "formik"; import { Plus, Trash, TriangleAlert } from "lucide-react"; -import { type FC, useId, useState } from "react"; +import { type FC, type KeyboardEventHandler, useId, useState } from "react"; import { docs } from "utils/docs"; import { isUUID } from "utils/uuid"; import * as Yup from "yup"; @@ -70,6 +70,7 @@ interface IdpGroupSyncFormProps { legacyGroupMappingCount: number; organization: Organization; onSubmit: (data: GroupSyncSettings) => void; + onSyncFieldChange: (value: string) => void; } export const IdpGroupSyncForm: FC = ({ @@ -81,6 +82,7 @@ export const IdpGroupSyncForm: FC = ({ groupsMap, organization, onSubmit, + onSyncFieldChange, }) => { const form = useFormik({ initialValues: { @@ -97,6 +99,8 @@ export const IdpGroupSyncForm: FC = ({ const [idpGroupName, setIdpGroupName] = useState(""); const [coderGroups, setCoderGroups] = useState([]); const id = useId(); + const [comboInputValue, setComboInputValue] = useState(""); + const [open, setOpen] = useState(false); const getGroupNames = (groupIds: readonly string[]) => { return groupIds.map((groupId) => groupsMap.get(groupId) || groupId); @@ -116,6 +120,21 @@ export const IdpGroupSyncForm: FC = ({ form.handleSubmit(); }; + const handleKeyDown: KeyboardEventHandler = (event) => { + if ( + event.key === "Enter" && + comboInputValue && + !claimFieldValues?.some( + (value) => value === comboInputValue.toLowerCase(), + ) + ) { + event.preventDefault(); + setIdpGroupName(comboInputValue); + setComboInputValue(""); + setOpen(false); + } + }; + return (
= ({ value={form.values.field} onChange={(event) => { void form.setFieldValue("field", event.target.value); + onSyncFieldChange(event.target.value); }} className="w-72" /> @@ -202,14 +222,31 @@ export const IdpGroupSyncForm: FC = ({ - { - setIdpGroupName(event.target.value); - }} - /> + {claimFieldValues ? ( + { + setIdpGroupName(value); + setOpen(false); + }} + /> + ) : ( + { + setIdpGroupName(event.target.value); + }} + /> + )}