diff --git a/site/src/api/api.ts b/site/src/api/api.ts index cd21b5b063ac6..5a314ddde151a 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -698,7 +698,7 @@ class ApiMethods { } const response = await this.axios.get( - `/api/v2/organizations/${organization}/provisionerdaemons?${params.toString()}`, + `/api/v2/organizations/${organization}/provisionerdaemons?${params}`, ); return response.data; }; @@ -787,19 +787,25 @@ class ApiMethods { return response.data; }; - getIdpSyncClaimFieldValues = async (claimField: string) => { - const response = await this.axios.get( - `/api/v2/settings/idpsync/field-values?claimField=${claimField}`, + getDeploymentIdpSyncFieldValues = async ( + field: string, + ): Promise => { + const params = new URLSearchParams(); + params.set("claimField", field); + const response = await this.axios.get( + `/api/v2/settings/idpsync/field-values?${params}`, ); return response.data; }; - getIdpSyncClaimFieldValuesByOrganization = async ( + getOrganizationIdpSyncClaimFieldValues = async ( organization: string, - claimField: string, + field: string, ) => { + const params = new URLSearchParams(); + params.set("claimField", field); const response = await this.axios.get( - `/api/v2/organizations/${organization}/settings/idpsync/field-values?claimField=${claimField}`, + `/api/v2/organizations/${organization}/settings/idpsync/field-values?${params}`, ); return response.data; }; diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 62449af12fccf..999dd2ee4cbd5 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -29,3 +29,10 @@ export const deploymentSSHConfig = () => { queryFn: API.getDeploymentSSHConfig, }; }; + +export const deploymentIdpSyncFieldValues = (field: string) => { + return { + queryKey: ["deployment", "idpSync", "fieldValues", field], + queryFn: () => API.getDeploymentIdpSyncFieldValues(field), + }; +}; diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 33ef19f0d2654..6246664e6ecf0 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -341,32 +341,16 @@ export const organizationsPermissions = ( export const getOrganizationIdpSyncClaimFieldValuesKey = ( organization: string, - claimField: string, -) => [organization, claimField, "organizationIdpSyncClaimFieldValues"]; + field: string, +) => [organization, "idpSync", "fieldValues", field]; export const organizationIdpSyncClaimFieldValues = ( organization: string, - claimField: string, + field: string, ) => { return { - queryKey: getOrganizationIdpSyncClaimFieldValuesKey( - organization, - claimField, - ), + queryKey: getOrganizationIdpSyncClaimFieldValuesKey(organization, field), 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, + API.getOrganizationIdpSyncClaimFieldValues(organization, field), }; }; diff --git a/site/src/components/Combobox/Combobox.tsx b/site/src/components/Combobox/Combobox.tsx index f5447b3a87062..fa15b6808a05e 100644 --- a/site/src/components/Combobox/Combobox.tsx +++ b/site/src/components/Combobox/Combobox.tsx @@ -18,7 +18,7 @@ import { cn } from "utils/cn"; interface ComboboxProps { value: string; - options?: string[]; + options?: readonly string[]; placeholder?: string; open: boolean; onOpenChange: (open: boolean) => void; diff --git a/site/src/components/Tooltip/Tooltip.stories.tsx b/site/src/components/Tooltip/Tooltip.stories.tsx index 68561b6a189e3..9af79ca76c099 100644 --- a/site/src/components/Tooltip/Tooltip.stories.tsx +++ b/site/src/components/Tooltip/Tooltip.stories.tsx @@ -12,16 +12,12 @@ const meta: Meta = { component: TooltipProvider, args: { children: ( - <> - - - - - - Add to library - - - + + + + + Add to library + ), }, }; diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx index 4d5b53e0f3ea2..295b482f94286 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx @@ -1,9 +1,9 @@ import { getErrorMessage } from "api/errors"; +import { deploymentIdpSyncFieldValues } from "api/queries/deployment"; 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"; @@ -21,26 +21,23 @@ import { ExportPolicyButton } from "./ExportPolicyButton"; import IdpOrgSyncPageView from "./IdpOrgSyncPageView"; export const IdpOrgSyncPage: FC = () => { - const [claimField, setClaimField] = useState(""); const queryClient = useQueryClient(); // IdP sync does not have its own entitlement and is based on templace_rbac const { template_rbac: isIdpSyncEnabled } = useFeatureVisibility(); const { organizations } = useDashboard(); - const { - data: orgSyncSettingsData, - isLoading, - error, - } = useQuery({ - ...organizationIdpSyncSettings(isIdpSyncEnabled), - onSuccess: (data) => { - if (data?.field) { - setClaimField(data.field); - } - }, - }); + const settingsQuery = useQuery(organizationIdpSyncSettings(isIdpSyncEnabled)); - const { data: claimFieldValues } = useQuery( - idpSyncClaimFieldValues(claimField), + const [field, setField] = useState(""); + useEffect(() => { + if (!settingsQuery.data) { + return; + } + + setField(settingsQuery.data.field); + }, [settingsQuery.data]); + + const fieldValuesQuery = useQuery( + field ? deploymentIdpSyncFieldValues(field) : { enabled: false }, ); const patchOrganizationSyncSettingsMutation = useMutation( @@ -58,14 +55,10 @@ export const IdpOrgSyncPage: FC = () => { } }, [patchOrganizationSyncSettingsMutation.error]); - if (isLoading) { + if (settingsQuery.isLoading) { return ; } - const handleSyncFieldChange = (value: string) => { - setClaimField(value); - }; - return ( <> @@ -84,7 +77,7 @@ export const IdpOrgSyncPage: FC = () => {

- + @@ -96,8 +89,10 @@ export const IdpOrgSyncPage: FC = () => { { try { await patchOrganizationSyncSettingsMutation.mutateAsync(data); @@ -111,9 +106,7 @@ export const IdpOrgSyncPage: FC = () => { ); } }} - onSyncFieldChange={handleSyncFieldChange} - claimFieldValues={claimFieldValues} - error={error || patchOrganizationSyncSettingsMutation.error} + error={settingsQuery.error || fieldValuesQuery.error} /> diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx index 8d02e1f248833..78842737e5baf 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx @@ -5,12 +5,19 @@ import { MockOrganization2, MockOrganizationSyncSettings, MockOrganizationSyncSettings2, + MockOrganizationSyncSettingsEmpty, } from "testHelpers/entities"; import { IdpOrgSyncPageView } from "./IdpOrgSyncPageView"; const meta: Meta = { title: "pages/IdpOrgSyncPageView", component: IdpOrgSyncPageView, + args: { + organizationSyncSettings: MockOrganizationSyncSettings2, + claimFieldValues: Object.keys(MockOrganizationSyncSettings2.mapping), + organizations: [MockOrganization, MockOrganization2], + error: undefined, + }, }; export default meta; @@ -18,35 +25,29 @@ type Story = StoryObj; export const Empty: Story = { args: { - organizationSyncSettings: { - field: "", - mapping: {}, - organization_assign_default: true, - }, - organizations: [MockOrganization, MockOrganization2], - error: undefined, + organizationSyncSettings: MockOrganizationSyncSettingsEmpty, }, }; -export const Default: Story = { - args: { - organizationSyncSettings: MockOrganizationSyncSettings2, - organizations: [MockOrganization, MockOrganization2], - error: undefined, - }, -}; +export const Default: Story = {}; export const HasError: Story = { args: { - ...Default.args, error: "This is a test error", }, }; export const MissingGroups: Story = { args: { - ...Default.args, organizationSyncSettings: MockOrganizationSyncSettings, + claimFieldValues: Object.keys(MockOrganizationSyncSettings.mapping), + organizations: [], + }, +}; + +export const MissingClaim: Story = { + args: { + claimFieldValues: [], }, }; diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index 031234da0da25..f6822ba0a60ef 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -1,3 +1,4 @@ +import { TooltipProvider } from "@radix-ui/react-tooltip"; import type { Organization, OrganizationSyncSettings, @@ -28,12 +29,8 @@ import { MultiSelectCombobox, type Option, } from "components/MultiSelectCombobox/MultiSelectCombobox"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "components/Popover/Popover"; import { Spinner } from "components/Spinner/Spinner"; +import { Stack } from "components/Stack/Stack"; import { Switch } from "components/Switch/Switch"; import { Table, @@ -42,10 +39,14 @@ import { TableHeader, TableRow, } from "components/Table/Table"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; import { useFormik } from "formik"; -import { Check, ChevronDown, CornerDownLeft, Plus, Trash } from "lucide-react"; +import { Plus, Trash, TriangleAlert } from "lucide-react"; import { type FC, type KeyboardEventHandler, useId, useState } from "react"; -import { cn } from "utils/cn"; import { docs } from "utils/docs"; import { isUUID } from "utils/uuid"; import * as Yup from "yup"; @@ -53,10 +54,10 @@ import { OrganizationPills } from "./OrganizationPills"; interface IdpSyncPageViewProps { organizationSyncSettings: OrganizationSyncSettings | undefined; + claimFieldValues: readonly string[] | undefined; organizations: readonly Organization[]; onSubmit: (data: OrganizationSyncSettings) => void; onSyncFieldChange: (value: string) => void; - claimFieldValues: string[] | undefined; error?: unknown; } @@ -84,10 +85,10 @@ const validationSchema = Yup.object({ export const IdpOrgSyncPageView: FC = ({ organizationSyncSettings, + claimFieldValues, organizations, onSubmit, onSyncFieldChange, - claimFieldValues, error, }) => { const form = useFormik({ @@ -313,6 +314,7 @@ export const IdpOrgSyncPageView: FC = ({ idpOrg={idpOrg} coderOrgs={getOrgNames(organizations)} onDelete={handleDelete} + exists={claimFieldValues?.includes(idpOrg)} /> ))} @@ -398,18 +400,43 @@ const IdpMappingTable: FC = ({ isEmpty, children }) => { interface OrganizationRowProps { idpOrg: string; + exists: boolean | undefined; coderOrgs: readonly string[]; onDelete: (idpOrg: string) => void; } const OrganizationRow: FC = ({ idpOrg, + exists = true, coderOrgs, onDelete, }) => { return ( - {idpOrg} + +
+ {idpOrg} + {!exists && ( + + + + + + + This value has not be seen in the specified claim field + before. You might want to check your IdP configuration and + ensure that this value is not misspelled. + + + + )} +
+
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index c522457a63c1d..d8ce878bdef6e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2720,6 +2720,13 @@ export const MockOrganizationSyncSettings2: TypesGen.OrganizationSyncSettings = organization_assign_default: true, }; +export const MockOrganizationSyncSettingsEmpty: TypesGen.OrganizationSyncSettings = + { + field: "", + mapping: {}, + organization_assign_default: true, + }; + export const MockGroup: TypesGen.Group = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", name: "Front-End", diff --git a/site/tailwind.config.js b/site/tailwind.config.js index b9964e053fda3..e47048a8b2512 100644 --- a/site/tailwind.config.js +++ b/site/tailwind.config.js @@ -33,6 +33,7 @@ module.exports = { success: "hsl(var(--content-success))", danger: "hsl(var(--content-danger))", link: "hsl(var(--content-link))", + warning: "hsl(var(--content-warning))", }, surface: { primary: "hsl(var(--surface-primary))",