Skip to content
20 changes: 13 additions & 7 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ class ApiMethods {
}

const response = await this.axios.get<TypesGen.ProvisionerDaemon[]>(
`/api/v2/organizations/${organization}/provisionerdaemons?${params.toString()}`,
`/api/v2/organizations/${organization}/provisionerdaemons?${params}`,
);
return response.data;
};
Expand Down Expand Up @@ -787,19 +787,25 @@ class ApiMethods {
return response.data;
};

getIdpSyncClaimFieldValues = async (claimField: string) => {
const response = await this.axios.get<string[]>(
`/api/v2/settings/idpsync/field-values?claimField=${claimField}`,
getDeploymentIdpSyncFieldValues = async (
field: string,
): Promise<readonly string[]> => {
const params = new URLSearchParams();
params.set("claimField", field);
const response = await this.axios.get<readonly string[]>(
`/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<TypesGen.Response>(
`/api/v2/organizations/${organization}/settings/idpsync/field-values?claimField=${claimField}`,
`/api/v2/organizations/${organization}/settings/idpsync/field-values?${params}`,
);
return response.data;
};
Expand Down
7 changes: 7 additions & 0 deletions site/src/api/queries/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
};
26 changes: 5 additions & 21 deletions site/src/api/queries/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
};
2 changes: 1 addition & 1 deletion site/src/components/Combobox/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 6 additions & 10 deletions site/src/components/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,12 @@ const meta: Meta<typeof TooltipProvider> = {
component: TooltipProvider,
args: {
children: (
<>
<TooltipProvider>
<Tooltip open>
<TooltipTrigger asChild>
<Button variant="outline">Hover</Button>
</TooltipTrigger>
<TooltipContent>Add to library</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
<Tooltip open>
<TooltipTrigger asChild>
<Button variant="outline">Hover</Button>
</TooltipTrigger>
<TooltipContent>Add to library</TooltipContent>
</Tooltip>
),
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(
Expand All @@ -58,14 +55,10 @@ export const IdpOrgSyncPage: FC = () => {
}
}, [patchOrganizationSyncSettingsMutation.error]);

if (isLoading) {
if (settingsQuery.isLoading) {
return <Loader />;
}

const handleSyncFieldChange = (value: string) => {
setClaimField(value);
};

return (
<>
<Helmet>
Expand All @@ -84,7 +77,7 @@ export const IdpOrgSyncPage: FC = () => {
</Link>
</p>
</div>
<ExportPolicyButton syncSettings={orgSyncSettingsData} />
<ExportPolicyButton syncSettings={settingsQuery.data} />
</header>
<ChooseOne>
<Cond condition={!isIdpSyncEnabled}>
Expand All @@ -96,8 +89,10 @@ export const IdpOrgSyncPage: FC = () => {
</Cond>
<Cond>
<IdpOrgSyncPageView
organizationSyncSettings={orgSyncSettingsData}
organizationSyncSettings={settingsQuery.data}
claimFieldValues={fieldValuesQuery.data}
organizations={organizations}
onSyncFieldChange={setField}
onSubmit={async (data) => {
try {
await patchOrganizationSyncSettingsMutation.mutateAsync(data);
Expand All @@ -111,9 +106,7 @@ export const IdpOrgSyncPage: FC = () => {
);
}
}}
onSyncFieldChange={handleSyncFieldChange}
claimFieldValues={claimFieldValues}
error={error || patchOrganizationSyncSettingsMutation.error}
error={settingsQuery.error || fieldValuesQuery.error}
/>
</Cond>
</ChooseOne>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,49 @@ import {
MockOrganization2,
MockOrganizationSyncSettings,
MockOrganizationSyncSettings2,
MockOrganizationSyncSettingsEmpty,
} from "testHelpers/entities";
import { IdpOrgSyncPageView } from "./IdpOrgSyncPageView";

const meta: Meta<typeof IdpOrgSyncPageView> = {
title: "pages/IdpOrgSyncPageView",
component: IdpOrgSyncPageView,
args: {
organizationSyncSettings: MockOrganizationSyncSettings2,
claimFieldValues: Object.keys(MockOrganizationSyncSettings2.mapping),
organizations: [MockOrganization, MockOrganization2],
error: undefined,
},
};

export default meta;
type Story = StoryObj<typeof IdpOrgSyncPageView>;

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: [],
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TooltipProvider } from "@radix-ui/react-tooltip";
import type {
Organization,
OrganizationSyncSettings,
Expand Down Expand Up @@ -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,
Expand All @@ -42,21 +39,25 @@ 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";
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;
}

Expand Down Expand Up @@ -84,10 +85,10 @@ const validationSchema = Yup.object({

export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
organizationSyncSettings,
claimFieldValues,
organizations,
onSubmit,
onSyncFieldChange,
claimFieldValues,
error,
}) => {
const form = useFormik<OrganizationSyncSettings>({
Expand Down Expand Up @@ -313,6 +314,7 @@ export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
idpOrg={idpOrg}
coderOrgs={getOrgNames(organizations)}
onDelete={handleDelete}
exists={claimFieldValues?.includes(idpOrg)}
/>
))}
</IdpMappingTable>
Expand Down Expand Up @@ -398,18 +400,43 @@ const IdpMappingTable: FC<IdpMappingTableProps> = ({ isEmpty, children }) => {

interface OrganizationRowProps {
idpOrg: string;
exists: boolean | undefined;
coderOrgs: readonly string[];
onDelete: (idpOrg: string) => void;
}

const OrganizationRow: FC<OrganizationRowProps> = ({
idpOrg,
exists = true,
coderOrgs,
onDelete,
}) => {
return (
<TableRow data-testid={`idp-org-${idpOrg}`}>
<TableCell>{idpOrg}</TableCell>
<TableCell>
<div className="flex flex-row items-center gap-2 text-content-primary">
{idpOrg}
{!exists && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlert className="size-icon-xs cursor-pointer text-content-warning" />
</TooltipTrigger>
<TooltipContent
align="start"
alignOffset={-8}
sideOffset={8}
className="p-2 text-xs text-content-secondary max-w-sm"
>
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.
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</TableCell>
<TableCell>
<OrganizationPills organizations={coderOrgs} />
</TableCell>
Expand Down
Loading
Loading