Skip to content

feat: orgs IDP sync - add combobox to select claim field value when sync field is set #16335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 5, 2025
Merged
Next Next commit
feat: add dropdown to select claim field value when sync field is set
  • Loading branch information
jaaydenh committed Feb 4, 2025
commit e5d88c8a02755fe10e5eccea7c5383cef63d1941
17 changes: 17 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,23 @@ class ApiMethods {
return response.data;
};

getIdpSyncClaimFieldValues = async (claimField: string) => {
const response = await this.axios.get<string[]>(
`/api/v2/settings/idpsync/field-values?claimField=${claimField}`,
);
return response.data;
};

getIdpSyncClaimFieldValuesByOrganization = async (
organization: string,
claimField: string,
) => {
const response = await this.axios.get<TypesGen.Response>(
`/api/v2/organizations/${organization}/settings/idpsync/field-values?claimField=${claimField}`,
);
return response.data;
};

getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
const response = await this.axios.get<TypesGen.Template>(
`/api/v2/templates/${templateId}`,
Expand Down
32 changes: 32 additions & 0 deletions site/src/api/queries/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ export const MultiSelectCombobox = forwardRef<
>
<X className="h-5 w-5" />
</button>
<ChevronDown className="h-5 w-5 cursor-pointer text-content-secondary hover:text-content-primary" />
<ChevronDown className="size-icon-sm cursor-pointer text-content-secondary hover:text-content-primary" />
</div>
</div>
</div>
Expand Down
13 changes: 7 additions & 6 deletions site/src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@ export const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"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-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>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}
<SelectPrimitive.Icon asChild>
<ChevronDown className="size-icon-sm opacity-50" />
<ChevronDown className="size-icon-sm cursor-pointer text-content-secondary hover:text-content-primary" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
Expand Down Expand Up @@ -65,7 +66,7 @@ export const SelectScrollDownButton = React.forwardRef<
)}
{...props}
>
<ChevronDown className="size-icon-sm" />
<ChevronDown className="size-icon-sm cursor-pointer text-content-secondary hover:text-content-primary" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lot of duplication between this and the ChevronDown 35 lines up

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats sort of the way of Tailwind, duplication of styles is ok to a certain extent. It would feel strange to create a separate component just because 2 icons have the same styles.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't need to be a component, it could be a string constant or something

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracting classNames into strings is a pattern that I would argue we don't want to go down. I follow the idealogy that it is sometimes ok to have repeated classNames until it makes sense to use some technique like creating a new component to manage the duplication. https://v3.tailwindcss.com/docs/reusing-styles

</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
Expand Down
9 changes: 8 additions & 1 deletion site/src/modules/management/OrganizationSidebarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -147,6 +147,13 @@ const OrganizationsSettingsNavigation: FC<
<span className="truncate">
{organization?.display_name || organization?.name}
</span>
{activeOrganization.name === organization.name && (
<Check
size={16}
strokeWidth={2}
className="ml-auto"
/>
)}
</CommandItem>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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),
Expand All @@ -49,6 +55,10 @@ export const IdpOrgSyncPage: FC = () => {
return <Loader />;
}

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

return (
<>
<Helmet>
Expand Down Expand Up @@ -94,6 +104,8 @@ export const IdpOrgSyncPage: FC = () => {
);
}
}}
onSyncFieldChange={handleSyncFieldChange}
claimFieldValues={claimFieldValues}
error={error || patchOrganizationSyncSettingsMutation.error}
/>
</Cond>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
}

Expand Down Expand Up @@ -76,6 +85,8 @@ export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
organizationSyncSettings,
organizations,
onSubmit,
onSyncFieldChange,
claimFieldValues,
error,
}) => {
const form = useFormik<OrganizationSyncSettings>({
Expand Down Expand Up @@ -135,6 +146,7 @@ export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
value={form.values.field}
onChange={(event) => {
void form.setFieldValue("field", event.target.value);
onSyncFieldChange(event.target.value);
}}
/>
<Button
Expand Down Expand Up @@ -190,14 +202,33 @@ export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
<Label className="text-sm" htmlFor={`${id}-idp-org-name`}>
IdP organization name
</Label>
<Input
id={`${id}-idp-org-name`}
value={idpOrgName}
className="min-w-72 w-72"
onChange={(event) => {
setIdpOrgName(event.target.value);
}}
/>

{claimFieldValues ? (
<Select
onValueChange={(event) => setIdpOrgName(event)}
value={idpOrgName}
>
<SelectTrigger id={`${id}-idp-org-name`} className="w-72">
<SelectValue placeholder="Select IdP organization" />
</SelectTrigger>
<SelectContent className="[&_*[role=option]>span]:end-2 [&_*[role=option]>span]:start-auto [&_*[role=option]]:pe-8 [&_*[role=option]]:ps-2">
{claimFieldValues.map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
id={`${id}-idp-org-name`}
value={idpOrgName}
className="w-72"
onChange={(event) => {
setIdpOrgName(event.target.value);
}}
/>
)}
</div>
<div className="grid items-center gap-1 flex-1">
<Label className="text-sm" htmlFor={`${id}-coder-org`}>
Expand Down