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: (
- <>
-
-
-
- Hover
-
- Add to library
-
-
- >
+
+
+ Hover
+
+ 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))",