Replug SAML configurations

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-24 11:15:31 +01:00
committed by Bryan Frimin
parent 600d763d18
commit b012b5ec42
24 changed files with 2230 additions and 121 deletions

View File

@@ -0,0 +1,123 @@
import {
graphql,
usePreloadedQuery,
useQueryLoader,
type PreloadedQuery,
} from "react-relay";
import type { SAMLSettingsPageQuery } from "./__generated__/SAMLSettingsPageQuery.graphql";
import { Suspense, use, useState } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
import { Breadcrumb, Button, Dialog, useDialogRef } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { SAMLConfigurationList } from "./_components/SAMLConfigurationList";
import { NewSAMLConfigurationForm } from "./_components/NewSAMLConfigurationForm";
import {
EditSAMLConfigurationForm,
samlConfigurationFormQuery,
} from "./_components/EditSAMLConfigurationForm";
import type { EditSAMLConfigurationFormQuery } from "./_components/__generated__/EditSAMLConfigurationFormQuery.graphql";
import { SAMLDomainVerifyDialog } from "./_components/SAMLDomainVerifyDialog";
export const samlSettingsPageQuery = graphql`
query SAMLSettingsPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) @required(action: THROW) {
...SAMLConfigurationListFragment
}
}
`;
export function SAMLSettingsPage(props: {
queryRef: PreloadedQuery<SAMLSettingsPageQuery>;
}) {
const { queryRef } = props;
const formDialogRef = useDialogRef();
const domainDialogRef = useDialogRef();
const [isEditing, setIsEditing] = useState<boolean>();
const [domainVerificationToken, setDomainVerificationToken] =
useState<string>();
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const { organization } = usePreloadedQuery(samlSettingsPageQuery, queryRef);
const [formQueryRef, loadFormQuery] =
useQueryLoader<EditSAMLConfigurationFormQuery>(samlConfigurationFormQuery);
const handleOpenFormDialog = (samlConfigurationId?: string) => {
setIsEditing(!!samlConfigurationId);
if (samlConfigurationId) {
loadFormQuery({ samlConfigurationId }, { fetchPolicy: "network-only" });
}
formDialogRef.current?.open();
};
const handleCloseFormDialog = () => {
setIsEditing(false);
formDialogRef.current?.close();
};
const handleOpenVerifyDomainDialog = (domainVerificationToken: string) => {
setDomainVerificationToken(domainVerificationToken);
domainDialogRef.current?.open();
};
const handleCloseVerifyDomainDialog = () => {
setDomainVerificationToken("");
formDialogRef.current?.close();
};
return (
<>
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-base font-medium">{__("SAML Single Sign-On")}</h2>
{isAuthorized("Organization", "createSAMLConfiguration") && (
<Button onClick={() => handleOpenFormDialog()}>
{__("Add Configuration")}
</Button>
)}
</div>
<SAMLConfigurationList
fKey={organization}
onNew={handleOpenFormDialog}
onEdit={(id: string) => handleOpenFormDialog(id)}
onVerifyDomain={handleOpenVerifyDomainDialog}
/>
</div>
<Dialog
ref={formDialogRef}
onClose={handleCloseFormDialog}
title={<Breadcrumb items={[__("SAML Settings"), __("Configure")]} />}
>
{isEditing ? (
<Suspense>
{formQueryRef && (
<EditSAMLConfigurationForm
queryRef={formQueryRef}
onUpdate={handleCloseFormDialog}
/>
)}
</Suspense>
) : (
<NewSAMLConfigurationForm onCreate={handleCloseFormDialog} />
)}
</Dialog>
<Dialog
ref={domainDialogRef}
onClose={handleCloseVerifyDomainDialog}
title={
<Breadcrumb items={[__("SAML Settings"), __("Verify Domain")]} />
}
>
{domainVerificationToken && (
<SAMLDomainVerifyDialog
key={domainVerificationToken}
domainVerificationToken={domainVerificationToken}
/>
)}
</Dialog>
</>
);
}

View File

@@ -0,0 +1,33 @@
import { useQueryLoader } from "react-relay";
import { SAMLSettingsPage, samlSettingsPageQuery } from "./SAMLSettingsPage";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { IAMRelayProvider } from "/providers/IAMRelayProvider";
import { useEffect } from "react";
import type { SAMLSettingsPageQuery } from "./__generated__/SAMLSettingsPageQuery.graphql";
function SAMLSettingsPageLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<SAMLSettingsPageQuery>(
samlSettingsPageQuery,
);
useEffect(() => {
loadQuery({
organizationId,
});
}, [loadQuery, organizationId]);
if (!queryRef) {
return null;
}
return <SAMLSettingsPage queryRef={queryRef} />;
}
export default function () {
return (
<IAMRelayProvider>
<SAMLSettingsPageLoader />
</IAMRelayProvider>
);
}

View File

@@ -0,0 +1,245 @@
/**
* @generated SignedSource<<57eb2489b4c62cb4b1df1a61bc135313>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type SAMLSettingsPageQuery$variables = {
organizationId: string;
};
export type SAMLSettingsPageQuery$data = {
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"SAMLConfigurationListFragment">;
};
};
export type SAMLSettingsPageQuery = {
response: SAMLSettingsPageQuery$data;
variables: SAMLSettingsPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v3 = [
{
"kind": "Literal",
"name": "first",
"value": 1000
}
],
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "SAMLSettingsPageQuery",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "SAMLConfigurationListFragment"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "SAMLSettingsPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v3/*: any*/),
"concreteType": "SAMLConfigurationConnection",
"kind": "LinkedField",
"name": "samlConfigurations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SAMLConfigurationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SAMLConfiguration",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailDomain",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "enforcementPolicy",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerificationToken",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerifiedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "testLoginUrl",
"storageKey": null
},
(v2/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "samlConfigurations(first:1000)"
},
{
"alias": null,
"args": (v3/*: any*/),
"filters": null,
"handle": "connection",
"key": "SAMLConfigurationListFragment_samlConfigurations",
"kind": "LinkedHandle",
"name": "samlConfigurations"
}
],
"type": "Organization",
"abstractKey": null
},
(v4/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "45740d8e7e6fba1ec2576fd62487e41e",
"id": null,
"metadata": {},
"name": "SAMLSettingsPageQuery",
"operationKind": "query",
"text": "query SAMLSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ...SAMLConfigurationListFragment\n id\n }\n}\n\nfragment SAMLConfigurationListFragment on Organization {\n samlConfigurations(first: 1000) {\n edges {\n node {\n id\n emailDomain\n enforcementPolicy\n domainVerificationToken\n domainVerifiedAt\n testLoginUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
}
};
})();
(node as any).hash = "d2d6ef2a90f81a75df7ab0dd83ef52cf";
export default node;

View File

@@ -0,0 +1,111 @@
import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import type { EditSAMLConfigurationForm_updateMutation } from "./__generated__/EditSAMLConfigurationForm_updateMutation.graphql";
import { useCallback } from "react";
import type { EditSAMLConfigurationFormQuery } from "./__generated__/EditSAMLConfigurationFormQuery.graphql";
import { useOrganizationId } from "/hooks/useOrganizationId";
import {
SAMLConfigurationForm,
type SAMLConfigurationFormData,
} from "./SAMLConfigurationForm";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
export const samlConfigurationFormQuery = graphql`
query EditSAMLConfigurationFormQuery($samlConfigurationId: ID!) {
samlConfiguration: node(id: $samlConfigurationId) @required(action: THROW) {
__typename
... on SAMLConfiguration {
id
emailDomain
enforcementPolicy
domainVerificationToken
domainVerifiedAt
testLoginUrl
idpEntityId
idpSsoUrl
idpCertificate
attributeMappings {
email
firstName
lastName
role
}
autoSignupEnabled
}
}
}
`;
const updateSAMLConfigurationMutation = graphql`
mutation EditSAMLConfigurationForm_updateMutation(
$input: UpdateSAMLConfigurationInput!
) {
updateSAMLConfiguration(input: $input) {
samlConfiguration {
id
emailDomain
enforcementPolicy
domainVerificationToken
domainVerifiedAt
testLoginUrl
}
}
}
`;
export function EditSAMLConfigurationForm(props: {
onUpdate: () => void;
queryRef: PreloadedQuery<EditSAMLConfigurationFormQuery>;
}) {
const { onUpdate, queryRef } = props;
const organizationId = useOrganizationId();
const { samlConfiguration } =
usePreloadedQuery<EditSAMLConfigurationFormQuery>(
samlConfigurationFormQuery,
queryRef,
);
if (samlConfiguration.__typename !== "SAMLConfiguration") {
throw new Error("node is not a SAML configuration");
}
const [update, isUpdating] =
useMutationWithToasts<EditSAMLConfigurationForm_updateMutation>(
updateSAMLConfigurationMutation,
{
successMessage: "SAML configuration updated successfully.",
errorMessage: "Failed to update SAML configuration. Please try again.",
},
);
const handleUpdate = useCallback(
(data: SAMLConfigurationFormData) => {
update({
variables: {
input: {
samlConfigurationId: samlConfiguration.id,
organizationId,
idpEntityId: data.idpEntityId,
idpSsoUrl: data.idpSsoUrl,
idpCertificate: data.idpCertificate,
autoSignupEnabled: data.autoSignupEnabled,
enforcementPolicy: data.enforcementPolicy,
attributeMappings: data.attributeMappings,
},
},
onCompleted: onUpdate,
});
},
[onUpdate, organizationId, samlConfiguration.id, update],
);
return (
<SAMLConfigurationForm
disabled={isUpdating}
initialValues={samlConfiguration}
isEditing
onSubmit={handleUpdate}
/>
);
}

View File

@@ -0,0 +1,73 @@
import { ConnectionHandler, graphql } from "react-relay";
import type { NewSAMLConfigurationForm_createMutation } from "./__generated__/NewSAMLConfigurationForm_createMutation.graphql";
import {
SAMLConfigurationForm,
type SAMLConfigurationFormData,
} from "./SAMLConfigurationForm";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { useCallback } from "react";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
const createSAMLConfigurationMutation = graphql`
mutation NewSAMLConfigurationForm_createMutation(
$input: CreateSAMLConfigurationInput!
$connections: [ID!]!
) {
createSAMLConfiguration(input: $input) {
samlConfigurationEdge @prependEdge(connections: $connections) {
node {
id
emailDomain
enforcementPolicy
domainVerificationToken
domainVerifiedAt
testLoginUrl
}
}
}
}
`;
export function NewSAMLConfigurationForm(props: { onCreate: () => void }) {
const { onCreate } = props;
const organizationId = useOrganizationId();
const [create, isCreating] =
useMutationWithToasts<NewSAMLConfigurationForm_createMutation>(
createSAMLConfigurationMutation,
{
successMessage: "SAML configuration created successfully.",
errorMessage: "Failed to create SAML configuration",
},
);
const handleCreate = useCallback(
(data: SAMLConfigurationFormData) => {
const connectionID = ConnectionHandler.getConnectionID(
organizationId,
"SAMLConfigurationListFragment_samlConfigurations",
);
create({
variables: {
input: {
organizationId,
emailDomain: data.emailDomain,
idpEntityId: data.idpEntityId,
idpSsoUrl: data.idpSsoUrl,
idpCertificate: data.idpCertificate,
autoSignupEnabled: data.autoSignupEnabled,
attributeMappings: data.attributeMappings,
},
connections: [connectionID],
},
onCompleted: onCreate,
});
},
[organizationId, create, onCreate],
);
return (
<SAMLConfigurationForm onSubmit={handleCreate} disabled={isCreating} />
);
}

View File

@@ -0,0 +1,255 @@
import { useTranslate } from "@probo/i18n";
import {
Button,
Checkbox,
DialogContent,
DialogFooter,
Field,
Label,
Option,
Select,
Textarea,
} from "@probo/ui";
import { Controller } from "react-hook-form";
import z from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
const defaultValues: SAMLConfigurationFormData = {
emailDomain: "",
enforcementPolicy: "OPTIONAL" as const,
idpEntityId: "",
idpSsoUrl: "",
idpCertificate: "",
attributeMappings: {
email: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
firstName:
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
lastName: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
role: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
},
autoSignupEnabled: false,
};
const getEnforcementPolicyLabel = (
policy: string,
__: (key: string) => string,
) => {
switch (policy) {
case "OFF":
return __(
"Your team members can't use single sign-on and must use their password",
);
case "REQUIRED":
return __("Your team members must use single sign-on to log in");
case "OPTIONAL":
default:
return __(
"Your team members may use either single sign-on or their password to log in",
);
}
};
const samlConfigSchema = z.object({
emailDomain: z
.string()
.min(1, "Email domain is required")
.regex(
/^[a-z0-9.-]+\.[a-z]{2,}$/i,
"Must be a valid domain (e.g., example.com)",
),
enforcementPolicy: z.enum(["OFF", "OPTIONAL", "REQUIRED"]),
spCertificate: z.string().optional(),
spPrivateKey: z.string().optional(),
idpEntityId: z.string().min(1, "IdP Entity ID is required"),
idpSsoUrl: z.string().url("IdP SSO URL must be a valid URL"),
idpCertificate: z.string().min(1, "IdP Certificate is required"),
attributeMappings: z.object({
email: z.string().optional(),
firstName: z.string().optional(),
lastName: z.string().optional(),
role: z.string().optional(),
}),
autoSignupEnabled: z.boolean().default(false),
});
export type SAMLConfigurationFormData = z.infer<typeof samlConfigSchema>;
export function SAMLConfigurationForm(props: {
isEditing?: boolean;
disabled: boolean;
initialValues?: SAMLConfigurationFormData;
onSubmit: (data: SAMLConfigurationFormData) => void;
}) {
const {
disabled,
initialValues = defaultValues,
isEditing,
onSubmit,
} = props;
const { __ } = useTranslate();
const form = useFormWithSchema(samlConfigSchema, {
defaultValues: initialValues,
});
return (
<form
onSubmit={async (e) => {
e.preventDefault();
await form.handleSubmit(onSubmit)();
form.reset(form.getValues());
}}
>
<DialogContent padded className="space-y-6">
<div>
<h3 className="text-base font-medium mb-4">
{__("Basic Configuration")}
</h3>
<div className="space-y-4">
<div>
<Field
{...form.register("emailDomain")}
label={__("Email Domain") + " *"}
placeholder="example.com"
disabled={isEditing}
error={form.formState.errors.emailDomain?.message}
/>
<p className="text-xs text-gray-600 mt-1">
{isEditing
? __("Email domain cannot be changed after creation")
: __(
"The email domain this SAML configuration applies to (e.g., example.com)",
)}
</p>
</div>
{isEditing && (
<div>
<Label htmlFor="enforcementPolicy">
{__("Enforcement Policy") + " *"}
</Label>
<Controller
control={form.control}
name="enforcementPolicy"
render={({ field }) => (
<div className="mt-2">
<Select
value={field.value}
onValueChange={field.onChange}
>
<Option value="OPTIONAL">{__("Optional")}</Option>
<Option value="REQUIRED">{__("Required")}</Option>
<Option value="OFF">{__("Off")}</Option>
</Select>
</div>
)}
/>
{form.watch("enforcementPolicy") && (
<p className="text-xs text-gray-600 mt-2">
{getEnforcementPolicyLabel(
form.watch("enforcementPolicy"),
__,
)}
</p>
)}
</div>
)}
</div>
</div>
<div>
<h3 className="text-base font-medium mb-4">
{__("Identity Provider Configuration")}
</h3>
<div className="space-y-4">
<Field
{...form.register("idpEntityId")}
label={__("IdP Entity ID") + " *"}
placeholder="https://idp.example.com/metadata"
error={form.formState.errors.idpEntityId?.message}
/>
<Field
{...form.register("idpSsoUrl")}
label={__("IdP SSO URL") + " *"}
placeholder="https://idp.example.com/sso"
error={form.formState.errors.idpSsoUrl?.message}
/>
<div>
<Label htmlFor="idpCertificate">
{__("IdP X.509 Certificate") + " *"}
</Label>
<Textarea
{...form.register("idpCertificate")}
id="idpCertificate"
rows={6}
placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----"
className="font-mono text-sm"
/>
{form.formState.errors.idpCertificate && (
<p className="text-sm text-red-600 mt-1">
{form.formState.errors.idpCertificate.message}
</p>
)}
</div>
</div>
</div>
<div>
<h3 className="text-base font-medium mb-4">
{__("Attribute Mapping")}
</h3>
<div className="space-y-4">
<Field
{...form.register("attributeMappings.email")}
label={__("Email Attribute")}
placeholder={defaultValues.attributeMappings.email}
error={form.formState.errors.attributeMappings?.email?.message}
/>
<Field
{...form.register("attributeMappings.firstName")}
label={__("First Name Attribute")}
placeholder={defaultValues.attributeMappings.firstName}
error={
form.formState.errors.attributeMappings?.firstName?.message
}
/>
<Field
{...form.register("attributeMappings.lastName")}
label={__("Last Name Attribute")}
placeholder={defaultValues.attributeMappings.lastName}
error={form.formState.errors.attributeMappings?.lastName?.message}
/>
<Field
{...form.register("attributeMappings.role")}
label={__("Role Attribute")}
placeholder={defaultValues.attributeMappings.role}
error={form.formState.errors.attributeMappings?.role?.message}
/>
</div>
</div>
<div>
<Controller
control={form.control}
name="autoSignupEnabled"
render={({ field }) => (
<div className="flex items-center gap-2">
<Checkbox
checked={field.value ?? false}
onChange={field.onChange}
/>
<Label htmlFor="autoSignupEnabled" className="cursor-pointer">
{__("Enable automatic user signup via SAML")}
</Label>
</div>
)}
/>
</div>
</DialogContent>
<DialogFooter>
<Button type="submit" disabled={disabled}>
{isEditing ? __("Update Configuration") : __("Create Configuration")}
</Button>
</DialogFooter>
</form>
);
}

View File

@@ -0,0 +1,241 @@
import { useTranslate } from "@probo/i18n";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
import {
Button,
Card,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
useConfirm,
} from "@probo/ui";
import { graphql } from "relay-runtime";
import { useFragment } from "react-relay";
import type {
SAMLConfigurationListFragment$data,
SAMLConfigurationListFragment$key,
} from "./__generated__/SAMLConfigurationListFragment.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import type { SAMLConfigurationList_deleteMutation } from "./__generated__/SAMLConfigurationList_deleteMutation.graphql";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { NodeOf } from "/types";
import { useCopy } from "@probo/hooks";
const fragment = graphql`
fragment SAMLConfigurationListFragment on Organization {
samlConfigurations(first: 1000)
@required(action: THROW)
@connection(key: "SAMLConfigurationListFragment_samlConfigurations") {
edges @required(action: THROW) {
node {
id
emailDomain
enforcementPolicy
domainVerificationToken
domainVerifiedAt
testLoginUrl
}
}
}
}
`;
const deleteMutation = graphql`
mutation SAMLConfigurationList_deleteMutation(
$input: DeleteSAMLConfigurationInput!
) {
deleteSAMLConfiguration(input: $input) {
deletedSamlConfigurationId
}
}
`;
export function SAMLConfigurationList(props: {
fKey: SAMLConfigurationListFragment$key;
onNew: () => void;
onEdit: (id: string) => void;
onVerifyDomain: (dnsVerificationToken: string) => void;
}) {
const { fKey, onNew, onEdit, onVerifyDomain } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const confirm = useConfirm();
const [isCopied, copy] = useCopy();
const {
samlConfigurations: { edges: samlConfigurations },
} = useFragment<SAMLConfigurationListFragment$key>(fragment, fKey);
const [deleteSAMLConfiguration] =
useMutationWithToasts<SAMLConfigurationList_deleteMutation>(
deleteMutation,
{
successMessage: "SAML configuration deleted successfully.",
errorMessage: "Failed to delete SAML configuration. Please try again.",
},
);
const handleDelete = (
config: NodeOf<SAMLConfigurationListFragment$data["samlConfigurations"]>,
) => {
confirm(
async () => {
deleteSAMLConfiguration({
variables: {
input: {
organizationId,
samlConfigurationId: config.id,
},
},
});
},
{
title: __("Delete SAML Configuration"),
message: __(
"Are you sure you want to delete the SAML configuration for " +
config.emailDomain +
"? This action cannot be undone.",
),
label: __("Delete"),
variant: "danger",
},
);
};
if (samlConfigurations.length === 0) {
return (
<Card padded>
<div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2">
{__("No SAML Configurations")}
</h3>
<p className="text-gray-600 mb-6">
{__(
"Set up SAML 2.0 single sign-on for your organization by adding a configuration for each email domain.",
)}
</p>
{isAuthorized("Organization", "createSAMLConfiguration") && (
<Button onClick={onNew}>
{__("Add Your First Configuration")}
</Button>
)}
</div>
</Card>
);
}
return (
<Table>
<Thead>
<Tr>
<Th>{__("Email Domain")}</Th>
<Th>{__("Domain Status")}</Th>
<Th>{__("SAML Status")}</Th>
<Th>{__("Enforcement")}</Th>
<Th>{__("SSO URL")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{samlConfigurations.map(({ node: config }) => (
<Tr key={config.id}>
<Td>
<button
onClick={() => onEdit(config.id)}
className="font-semibold text-blue-600 hover:text-blue-800"
>
{config.emailDomain}
</button>
</Td>
<Td>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
config.domainVerifiedAt
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{config.domainVerifiedAt
? __("Verified")
: __("Pending Verification")}
</span>
</Td>
<Td>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
config.enforcementPolicy !== "OFF"
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
}`}
>
{config.enforcementPolicy !== "OFF"
? __("Enabled")
: __("Disabled")}
</span>
</Td>
<Td>{config.enforcementPolicy}</Td>
<Td>
{config.domainVerifiedAt && config.enforcementPolicy !== "OFF" ? (
<button
onClick={() => copy(config.testLoginUrl)}
className="text-blue-600 hover:text-blue-800"
>
{isCopied ? __("Copied!") : __("Copy URL")}
</button>
) : (
<span className="text-gray-400">—</span>
)}
</Td>
<Td width={180} className="text-end">
<div className="flex gap-2 justify-end">
{config.domainVerifiedAt ? (
<>
{isAuthorized(
"SAMLConfiguration",
"updateSAMLConfiguration",
) && (
<Button
variant="secondary"
onClick={() => onEdit(config.id)}
>
{__("Edit")}
</Button>
)}
</>
) : (
<>
{isAuthorized("Organization", "verifyDomain") &&
!!config.domainVerificationToken && (
<Button
variant="primary"
onClick={() =>
onVerifyDomain(config.domainVerificationToken!)
}
>
{__("Verify Domain")}
</Button>
)}
{isAuthorized("Organization", "deleteOrganization") && (
<Button
variant="danger"
onClick={() => handleDelete(config)}
>
{__("Delete")}
</Button>
)}
</>
)}
</div>
</Td>
</Tr>
))}
</Tbody>
</Table>
);
}

View File

@@ -0,0 +1,75 @@
import { useCopy } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { DialogContent, Button } from "@probo/ui";
export function SAMLDomainVerifyDialog(props: {
domainVerificationToken: string;
}) {
const { domainVerificationToken } = props;
const { __ } = useTranslate();
const dnsRecord = `probo-verification=${domainVerificationToken}`;
const [isCopied, copy] = useCopy();
return (
<>
<DialogContent padded className="space-y-6">
<div>
<h3 className="text-base font-medium mb-4">
{__("Verify Domain Ownership")}
</h3>
<p className="text-sm text-gray-600 mb-4">
{__(
"Add the following TXT record to your domain's DNS configuration to verify ownership:",
)}
</p>
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<div className="space-y-2">
<div>
<span className="font-semibold text-sm">
{__("Host/Name:")}
</span>
<code className="ml-2 bg-white px-2 py-1 rounded text-sm">
@
</code>
<span className="ml-2 text-xs text-gray-600">
{__("or use your domain name")}
</span>
</div>
<div>
<span className="font-semibold text-sm">{__("Type:")}</span>
<code className="ml-2 bg-white px-2 py-1 rounded text-sm">
TXT
</code>
</div>
<div>
<span className="font-semibold text-sm">{__("Value:")}</span>
<div className="mt-1 flex items-center gap-2">
<code className="flex-1 bg-white px-2 py-1 rounded text-sm break-all font-mono">
{dnsRecord}
</code>
<Button
type="button"
variant="secondary"
onClick={() => copy(dnsRecord)}
>
{isCopied ? __("Copied!") : __("Copy")}
</Button>
</div>
</div>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800">
<strong>{__("Note:")}</strong>{" "}
{__(
"DNS changes may take up to 48 hours to propagate, but typically complete within a few minutes.",
)}
</p>
</div>
</div>
</DialogContent>
</>
);
}

View File

@@ -0,0 +1,273 @@
/**
* @generated SignedSource<<3da2840acb86d86a80715539be18fba0>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED";
export type EditSAMLConfigurationFormQuery$variables = {
samlConfigurationId: string;
};
export type EditSAMLConfigurationFormQuery$data = {
readonly samlConfiguration: {
readonly __typename: "SAMLConfiguration";
readonly attributeMappings: {
readonly email: string;
readonly firstName: string;
readonly lastName: string;
readonly role: string;
};
readonly autoSignupEnabled: boolean;
readonly domainVerificationToken: string | null | undefined;
readonly domainVerifiedAt: any | null | undefined;
readonly emailDomain: string;
readonly enforcementPolicy: SAMLEnforcementPolicy;
readonly id: string;
readonly idpCertificate: string;
readonly idpEntityId: string;
readonly idpSsoUrl: string;
readonly testLoginUrl: string;
} | {
// This will never be '%other', but we need some
// value in case none of the concrete values match.
readonly __typename: "%other";
};
};
export type EditSAMLConfigurationFormQuery = {
response: EditSAMLConfigurationFormQuery$data;
variables: EditSAMLConfigurationFormQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "samlConfigurationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "samlConfigurationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailDomain",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "enforcementPolicy",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerificationToken",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerifiedAt",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "testLoginUrl",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "idpEntityId",
"storageKey": null
},
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "idpSsoUrl",
"storageKey": null
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "idpCertificate",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"concreteType": "SAMLAttributeMappings",
"kind": "LinkedField",
"name": "attributeMappings",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "firstName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lastName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "role",
"storageKey": null
}
],
"storageKey": null
},
v13 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "autoSignupEnabled",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "EditSAMLConfigurationFormQuery",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": "samlConfiguration",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
(v13/*: any*/)
],
"type": "SAMLConfiguration",
"abstractKey": null
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EditSAMLConfigurationFormQuery",
"selections": [
{
"alias": "samlConfiguration",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
(v13/*: any*/)
],
"type": "SAMLConfiguration",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "47c1ce92e8ba0395fa803b343d093879",
"id": null,
"metadata": {},
"name": "EditSAMLConfigurationFormQuery",
"operationKind": "query",
"text": "query EditSAMLConfigurationFormQuery(\n $samlConfigurationId: ID!\n) {\n samlConfiguration: node(id: $samlConfigurationId) {\n __typename\n ... on SAMLConfiguration {\n id\n emailDomain\n enforcementPolicy\n domainVerificationToken\n domainVerifiedAt\n testLoginUrl\n idpEntityId\n idpSsoUrl\n idpCertificate\n attributeMappings {\n email\n firstName\n lastName\n role\n }\n autoSignupEnabled\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "d31326e3e4423e62d8acde5735d9b578";
export default node;

View File

@@ -0,0 +1,159 @@
/**
* @generated SignedSource<<33cdb4d30c06225b656ba2b3f27830eb>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED";
export type UpdateSAMLConfigurationInput = {
attributeMappings?: SAMLAttributeMappingsInput | null | undefined;
autoSignupEnabled?: boolean | null | undefined;
enforcementPolicy: SAMLEnforcementPolicy;
idpCertificate?: string | null | undefined;
idpEntityId?: string | null | undefined;
idpSsoUrl?: string | null | undefined;
organizationId: string;
samlConfigurationId: string;
};
export type SAMLAttributeMappingsInput = {
email?: string | null | undefined;
firstName?: string | null | undefined;
lastName?: string | null | undefined;
role?: string | null | undefined;
};
export type EditSAMLConfigurationForm_updateMutation$variables = {
input: UpdateSAMLConfigurationInput;
};
export type EditSAMLConfigurationForm_updateMutation$data = {
readonly updateSAMLConfiguration: {
readonly samlConfiguration: {
readonly domainVerificationToken: string | null | undefined;
readonly domainVerifiedAt: any | null | undefined;
readonly emailDomain: string;
readonly enforcementPolicy: SAMLEnforcementPolicy;
readonly id: string;
readonly testLoginUrl: string;
} | null | undefined;
} | null | undefined;
};
export type EditSAMLConfigurationForm_updateMutation = {
response: EditSAMLConfigurationForm_updateMutation$data;
variables: EditSAMLConfigurationForm_updateMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "UpdateSAMLConfigurationPayload",
"kind": "LinkedField",
"name": "updateSAMLConfiguration",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SAMLConfiguration",
"kind": "LinkedField",
"name": "samlConfiguration",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailDomain",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "enforcementPolicy",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerificationToken",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerifiedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "testLoginUrl",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "EditSAMLConfigurationForm_updateMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EditSAMLConfigurationForm_updateMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "56d29623294c243a01bfbc2ab4310d28",
"id": null,
"metadata": {},
"name": "EditSAMLConfigurationForm_updateMutation",
"operationKind": "mutation",
"text": "mutation EditSAMLConfigurationForm_updateMutation(\n $input: UpdateSAMLConfigurationInput!\n) {\n updateSAMLConfiguration(input: $input) {\n samlConfiguration {\n id\n emailDomain\n enforcementPolicy\n domainVerificationToken\n domainVerifiedAt\n testLoginUrl\n }\n }\n}\n"
}
};
})();
(node as any).hash = "b98e701514c179bba0f0ce63aae0c0b8";
export default node;

View File

@@ -0,0 +1,211 @@
/**
* @generated SignedSource<<0d0fdbf079f1a72545fa363ce60cd2f7>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED";
export type CreateSAMLConfigurationInput = {
attributeMappings?: SAMLAttributeMappingsInput | null | undefined;
autoSignupEnabled: boolean;
emailDomain: string;
idpCertificate: string;
idpEntityId: string;
idpSsoUrl: string;
organizationId: string;
};
export type SAMLAttributeMappingsInput = {
email?: string | null | undefined;
firstName?: string | null | undefined;
lastName?: string | null | undefined;
role?: string | null | undefined;
};
export type NewSAMLConfigurationForm_createMutation$variables = {
connections: ReadonlyArray<string>;
input: CreateSAMLConfigurationInput;
};
export type NewSAMLConfigurationForm_createMutation$data = {
readonly createSAMLConfiguration: {
readonly samlConfigurationEdge: {
readonly node: {
readonly domainVerificationToken: string | null | undefined;
readonly domainVerifiedAt: any | null | undefined;
readonly emailDomain: string;
readonly enforcementPolicy: SAMLEnforcementPolicy;
readonly id: string;
readonly testLoginUrl: string;
};
};
} | null | undefined;
};
export type NewSAMLConfigurationForm_createMutation = {
response: NewSAMLConfigurationForm_createMutation$data;
variables: NewSAMLConfigurationForm_createMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "SAMLConfigurationEdge",
"kind": "LinkedField",
"name": "samlConfigurationEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SAMLConfiguration",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailDomain",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "enforcementPolicy",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerificationToken",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerifiedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "testLoginUrl",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "NewSAMLConfigurationForm_createMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateSAMLConfigurationPayload",
"kind": "LinkedField",
"name": "createSAMLConfiguration",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "NewSAMLConfigurationForm_createMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateSAMLConfigurationPayload",
"kind": "LinkedField",
"name": "createSAMLConfiguration",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "prependEdge",
"key": "",
"kind": "LinkedHandle",
"name": "samlConfigurationEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "969d5fb34995f5cce359c792cbe1ff49",
"id": null,
"metadata": {},
"name": "NewSAMLConfigurationForm_createMutation",
"operationKind": "mutation",
"text": "mutation NewSAMLConfigurationForm_createMutation(\n $input: CreateSAMLConfigurationInput!\n) {\n createSAMLConfiguration(input: $input) {\n samlConfigurationEdge {\n node {\n id\n emailDomain\n enforcementPolicy\n domainVerificationToken\n domainVerifiedAt\n testLoginUrl\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "5a3b2e5d219b40ca096eece75d7b63b3";
export default node;

View File

@@ -0,0 +1,180 @@
/**
* @generated SignedSource<<12fade1fd1f7f3ae53595970b6c2c47e>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED";
import { FragmentRefs } from "relay-runtime";
export type SAMLConfigurationListFragment$data = {
readonly samlConfigurations: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly domainVerificationToken: string | null | undefined;
readonly domainVerifiedAt: any | null | undefined;
readonly emailDomain: string;
readonly enforcementPolicy: SAMLEnforcementPolicy;
readonly id: string;
readonly testLoginUrl: string;
};
}>;
};
readonly " $fragmentType": "SAMLConfigurationListFragment";
};
export type SAMLConfigurationListFragment$key = {
readonly " $data"?: SAMLConfigurationListFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"SAMLConfigurationListFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"samlConfigurations"
]
}
]
},
"name": "SAMLConfigurationListFragment",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": "samlConfigurations",
"args": null,
"concreteType": "SAMLConfigurationConnection",
"kind": "LinkedField",
"name": "__SAMLConfigurationListFragment_samlConfigurations_connection",
"plural": false,
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "SAMLConfigurationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SAMLConfiguration",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailDomain",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "enforcementPolicy",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerificationToken",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerifiedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "testLoginUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Organization",
"abstractKey": null
};
(node as any).hash = "79203ccae6f8e3a1967b463f0f3f3c76";
export default node;

View File

@@ -0,0 +1,93 @@
/**
* @generated SignedSource<<4ce15b49974b7e512f334f47765caaa4>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeleteSAMLConfigurationInput = {
organizationId: string;
samlConfigurationId: string;
};
export type SAMLConfigurationList_deleteMutation$variables = {
input: DeleteSAMLConfigurationInput;
};
export type SAMLConfigurationList_deleteMutation$data = {
readonly deleteSAMLConfiguration: {
readonly deletedSamlConfigurationId: string;
} | null | undefined;
};
export type SAMLConfigurationList_deleteMutation = {
response: SAMLConfigurationList_deleteMutation$data;
variables: SAMLConfigurationList_deleteMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "DeleteSAMLConfigurationPayload",
"kind": "LinkedField",
"name": "deleteSAMLConfiguration",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deletedSamlConfigurationId",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "SAMLConfigurationList_deleteMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "SAMLConfigurationList_deleteMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "cb46bb31e5d10f0ddad38795a72b1ab1",
"id": null,
"metadata": {},
"name": "SAMLConfigurationList_deleteMutation",
"operationKind": "mutation",
"text": "mutation SAMLConfigurationList_deleteMutation(\n $input: DeleteSAMLConfigurationInput!\n) {\n deleteSAMLConfiguration(input: $input) {\n deletedSamlConfigurationId\n }\n}\n"
}
};
})();
(node as any).hash = "2ebbe76aae843f00570f7ae1848ee995";
export default node;