@@ -2,6 +2,7 @@ import { useLocation, useRouteError } from "react-router";
|
||||
import { IconPageCross } from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { AuthenticationRequiredError } from "/providers/RelayProviders";
|
||||
|
||||
const classNames = {
|
||||
wrapper: "py-10 text-center space-y-2 ",
|
||||
@@ -32,6 +33,27 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||
}
|
||||
}, [location, resetErrorBoundary]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error instanceof AuthenticationRequiredError) {
|
||||
window.location.href = error.redirectUrl;
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
if (error instanceof AuthenticationRequiredError) {
|
||||
return (
|
||||
<div className={classNames.wrapper}>
|
||||
<h1 className={classNames.title}>
|
||||
{__("Additional authentication required")}
|
||||
</h1>
|
||||
<p className={classNames.description}>
|
||||
{error.requiresSaml
|
||||
? __("Redirecting to SAML authentication...")
|
||||
: __("Redirecting to login...")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!error || (error && error.toString().includes("PAGE_NOT_FOUND"))) {
|
||||
return (
|
||||
<div className={classNames.wrapper}>
|
||||
|
||||
@@ -10,8 +10,6 @@ export const organizationViewQuery = graphql`
|
||||
id
|
||||
name
|
||||
...SettingsPageFragment
|
||||
...SettingsPageMembershipsFragment
|
||||
...SettingsPageInvitationsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
205
apps/console/src/hooks/graph/SAMLConfigurationGraph.ts
Normal file
205
apps/console/src/hooks/graph/SAMLConfigurationGraph.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { graphql } from "relay-runtime";
|
||||
import { useMutationWithToasts } from "../useMutationWithToasts";
|
||||
import type { SAMLConfigurationGraphCreateMutation } from "./__generated__/SAMLConfigurationGraphCreateMutation.graphql";
|
||||
import type { SAMLConfigurationGraphUpdateMutation } from "./__generated__/SAMLConfigurationGraphUpdateMutation.graphql";
|
||||
import type { SAMLConfigurationGraphDeleteMutation } from "./__generated__/SAMLConfigurationGraphDeleteMutation.graphql";
|
||||
import type { SAMLConfigurationGraphEnableMutation } from "./__generated__/SAMLConfigurationGraphEnableMutation.graphql";
|
||||
import type { SAMLConfigurationGraphDisableMutation } from "./__generated__/SAMLConfigurationGraphDisableMutation.graphql";
|
||||
import type { SAMLConfigurationGraphInitiateDomainVerificationMutation } from "./__generated__/SAMLConfigurationGraphInitiateDomainVerificationMutation.graphql";
|
||||
import type { SAMLConfigurationGraphVerifyDomainMutation } from "./__generated__/SAMLConfigurationGraphVerifyDomainMutation.graphql";
|
||||
|
||||
const createSAMLConfigurationMutation = graphql`
|
||||
mutation SAMLConfigurationGraphCreateMutation(
|
||||
$input: CreateSAMLConfigurationInput!
|
||||
) {
|
||||
createSAMLConfiguration(input: $input) {
|
||||
samlConfiguration {
|
||||
id
|
||||
enabled
|
||||
emailDomain
|
||||
enforcementPolicy
|
||||
spEntityId
|
||||
spAcsUrl
|
||||
spMetadataUrl
|
||||
testLoginUrl
|
||||
idpEntityId
|
||||
idpSsoUrl
|
||||
idpCertificate
|
||||
idpMetadataUrl
|
||||
attributeEmail
|
||||
attributeFirstname
|
||||
attributeLastname
|
||||
attributeRole
|
||||
defaultRole
|
||||
autoSignupEnabled
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const updateSAMLConfigurationMutation = graphql`
|
||||
mutation SAMLConfigurationGraphUpdateMutation(
|
||||
$input: UpdateSAMLConfigurationInput!
|
||||
) {
|
||||
updateSAMLConfiguration(input: $input) {
|
||||
samlConfiguration {
|
||||
id
|
||||
enabled
|
||||
emailDomain
|
||||
enforcementPolicy
|
||||
spEntityId
|
||||
spAcsUrl
|
||||
spMetadataUrl
|
||||
testLoginUrl
|
||||
idpEntityId
|
||||
idpSsoUrl
|
||||
idpCertificate
|
||||
idpMetadataUrl
|
||||
attributeEmail
|
||||
attributeFirstname
|
||||
attributeLastname
|
||||
attributeRole
|
||||
defaultRole
|
||||
autoSignupEnabled
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteSAMLConfigurationMutation = graphql`
|
||||
mutation SAMLConfigurationGraphDeleteMutation(
|
||||
$input: DeleteSAMLConfigurationInput!
|
||||
) {
|
||||
deleteSAMLConfiguration(input: $input) {
|
||||
deletedSAMLConfigurationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const enableSAMLMutation = graphql`
|
||||
mutation SAMLConfigurationGraphEnableMutation($input: EnableSAMLInput!) {
|
||||
enableSAML(input: $input) {
|
||||
samlConfiguration {
|
||||
id
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const disableSAMLMutation = graphql`
|
||||
mutation SAMLConfigurationGraphDisableMutation($input: DisableSAMLInput!) {
|
||||
disableSAML(input: $input) {
|
||||
samlConfiguration {
|
||||
id
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const initiateDomainVerificationMutation = graphql`
|
||||
mutation SAMLConfigurationGraphInitiateDomainVerificationMutation(
|
||||
$input: InitiateDomainVerificationInput!
|
||||
) {
|
||||
initiateDomainVerification(input: $input) {
|
||||
samlConfiguration {
|
||||
id
|
||||
emailDomain
|
||||
domainVerified
|
||||
domainVerificationToken
|
||||
domainVerifiedAt
|
||||
}
|
||||
dnsRecord
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const verifyDomainMutation = graphql`
|
||||
mutation SAMLConfigurationGraphVerifyDomainMutation(
|
||||
$input: VerifyDomainInput!
|
||||
) {
|
||||
verifyDomain(input: $input) {
|
||||
samlConfiguration {
|
||||
id
|
||||
domainVerified
|
||||
domainVerifiedAt
|
||||
}
|
||||
verified
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function useCreateSAMLConfigurationMutation() {
|
||||
return useMutationWithToasts<SAMLConfigurationGraphCreateMutation>(
|
||||
createSAMLConfigurationMutation,
|
||||
{
|
||||
successMessage: "SAML configuration created successfully.",
|
||||
errorMessage: "Failed to create SAML configuration. Please try again.",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useUpdateSAMLConfigurationMutation() {
|
||||
return useMutationWithToasts<SAMLConfigurationGraphUpdateMutation>(
|
||||
updateSAMLConfigurationMutation,
|
||||
{
|
||||
successMessage: "SAML configuration updated successfully.",
|
||||
errorMessage: "Failed to update SAML configuration. Please try again.",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useDeleteSAMLConfigurationMutation() {
|
||||
return useMutationWithToasts<SAMLConfigurationGraphDeleteMutation>(
|
||||
deleteSAMLConfigurationMutation,
|
||||
{
|
||||
successMessage: "SAML configuration deleted successfully.",
|
||||
errorMessage: "Failed to delete SAML configuration. Please try again.",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useEnableSAMLMutation() {
|
||||
return useMutationWithToasts<SAMLConfigurationGraphEnableMutation>(
|
||||
enableSAMLMutation,
|
||||
{
|
||||
successMessage: "SAML enabled successfully.",
|
||||
errorMessage: "Failed to enable SAML. Please try again.",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useDisableSAMLMutation() {
|
||||
return useMutationWithToasts<SAMLConfigurationGraphDisableMutation>(
|
||||
disableSAMLMutation,
|
||||
{
|
||||
successMessage: "SAML disabled successfully.",
|
||||
errorMessage: "Failed to disable SAML. Please try again.",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useInitiateDomainVerificationMutation() {
|
||||
return useMutationWithToasts<SAMLConfigurationGraphInitiateDomainVerificationMutation>(
|
||||
initiateDomainVerificationMutation,
|
||||
{
|
||||
successMessage: "Domain verification initiated. Please add the DNS record.",
|
||||
errorMessage: "Failed to initiate domain verification. Please try again.",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useVerifyDomainMutation() {
|
||||
return useMutationWithToasts<SAMLConfigurationGraphVerifyDomainMutation>(
|
||||
verifyDomainMutation,
|
||||
{
|
||||
successMessage: "Domain verified successfully!",
|
||||
errorMessage: "Domain verification failed. Please ensure the DNS record is properly configured.",
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<e961390ac138b066af89b5b02169d66e>>
|
||||
* @generated SignedSource<<220f85396d6d4c5c04f730d07aaa42e1>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -17,7 +17,7 @@ export type OrganizationGraph_ViewQuery$data = {
|
||||
readonly node: {
|
||||
readonly id?: string;
|
||||
readonly name?: string;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment" | "SettingsPageInvitationsFragment" | "SettingsPageMembershipsFragment">;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment">;
|
||||
};
|
||||
};
|
||||
export type OrganizationGraph_ViewQuery = {
|
||||
@@ -82,52 +82,50 @@ v7 = {
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
v8 = {
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 20
|
||||
},
|
||||
v9 = {
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "ASC",
|
||||
"field": "CREATED_AT"
|
||||
v8 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "ASC",
|
||||
"field": "CREATED_AT"
|
||||
}
|
||||
}
|
||||
},
|
||||
v10 = [
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/)
|
||||
],
|
||||
v11 = {
|
||||
v9 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "totalCount",
|
||||
"storageKey": null
|
||||
},
|
||||
v12 = {
|
||||
v10 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
},
|
||||
v13 = {
|
||||
v11 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "role",
|
||||
"storageKey": null
|
||||
},
|
||||
v14 = {
|
||||
v12 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
},
|
||||
v15 = {
|
||||
v13 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
@@ -166,7 +164,7 @@ v15 = {
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
v16 = {
|
||||
v14 = {
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
@@ -178,19 +176,8 @@ v16 = {
|
||||
}
|
||||
]
|
||||
},
|
||||
v17 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"statuses": [
|
||||
"PENDING",
|
||||
"EXPIRED"
|
||||
]
|
||||
}
|
||||
},
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/)
|
||||
v15 = [
|
||||
"orderBy"
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
@@ -216,16 +203,6 @@ return {
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "SettingsPageFragment"
|
||||
},
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "SettingsPageMembershipsFragment"
|
||||
},
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "SettingsPageInvitationsFragment"
|
||||
}
|
||||
],
|
||||
"type": "Organization",
|
||||
@@ -294,6 +271,146 @@ return {
|
||||
"name": "headquarterAddress",
|
||||
"storageKey": null
|
||||
},
|
||||
(v6/*: any*/),
|
||||
(v7/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v8/*: any*/),
|
||||
"concreteType": "MembershipConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "memberships",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v9/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "MembershipEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Membership",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v10/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "emailAddress",
|
||||
"storageKey": null
|
||||
},
|
||||
(v11/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "authMethod",
|
||||
"storageKey": null
|
||||
},
|
||||
(v6/*: any*/),
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v12/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v13/*: any*/),
|
||||
(v14/*: any*/)
|
||||
],
|
||||
"storageKey": "memberships(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v8/*: any*/),
|
||||
"filters": (v15/*: any*/),
|
||||
"handle": "connection",
|
||||
"key": "MembersSettingsTabMemberships_memberships",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "memberships"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v8/*: any*/),
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v9/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "InvitationEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Invitation",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v10/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v11/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "status",
|
||||
"storageKey": null
|
||||
},
|
||||
(v6/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "expiresAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "acceptedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v12/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v13/*: any*/),
|
||||
(v14/*: any*/)
|
||||
],
|
||||
"storageKey": "invitations(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v8/*: any*/),
|
||||
"filters": (v15/*: any*/),
|
||||
"handle": "connection",
|
||||
"key": "MembersSettingsTabInvitations_invitations",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "invitations"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -369,143 +486,157 @@ return {
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v6/*: any*/),
|
||||
(v7/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v10/*: any*/),
|
||||
"concreteType": "MembershipConnection",
|
||||
"args": null,
|
||||
"concreteType": "SAMLConfiguration",
|
||||
"kind": "LinkedField",
|
||||
"name": "memberships",
|
||||
"plural": false,
|
||||
"name": "samlConfigurations",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
(v11/*: any*/),
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "MembershipEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Membership",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v12/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "emailAddress",
|
||||
"storageKey": null
|
||||
},
|
||||
(v13/*: any*/),
|
||||
(v6/*: any*/),
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v14/*: any*/)
|
||||
],
|
||||
"kind": "ScalarField",
|
||||
"name": "enabled",
|
||||
"storageKey": null
|
||||
},
|
||||
(v15/*: any*/),
|
||||
(v16/*: any*/)
|
||||
],
|
||||
"storageKey": "memberships(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v10/*: any*/),
|
||||
"filters": [
|
||||
"orderBy"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "SettingsPageMemberships_memberships",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "memberships"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v17/*: any*/),
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v11/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "InvitationEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Invitation",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v12/*: any*/),
|
||||
(v13/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "status",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "expiresAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "acceptedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
(v6/*: any*/),
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v14/*: any*/)
|
||||
],
|
||||
"kind": "ScalarField",
|
||||
"name": "emailDomain",
|
||||
"storageKey": null
|
||||
},
|
||||
(v15/*: any*/),
|
||||
(v16/*: any*/)
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "enforcementPolicy",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "domainVerified",
|
||||
"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": "spEntityId",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "spAcsUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "spMetadataUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "testLoginUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpEntityId",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpSsoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpCertificate",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpMetadataUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeEmail",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeFirstname",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeLastname",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeRole",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "defaultRole",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "autoSignupEnabled",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\",\"EXPIRED\"]},first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v17/*: any*/),
|
||||
"filters": [
|
||||
"orderBy",
|
||||
"filter"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "SettingsPageInvitations_invitations",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "invitations"
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Organization",
|
||||
@@ -517,16 +648,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "3d38000d9d2105ef8ae3d56edf31b372",
|
||||
"cacheID": "782dc2328cea4f0c350427bda92a5615",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "OrganizationGraph_ViewQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n ...SettingsPageMembershipsFragment\n ...SettingsPageInvitationsFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n createdAt\n updatedAt\n}\n\nfragment SettingsPageInvitationsFragment on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}, filter: {statuses: [PENDING, EXPIRED]}) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n status\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SettingsPageMembershipsFragment on Organization {\n memberships(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
|
||||
"text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n }\n id\n }\n}\n\nfragment DomainSettingsTabFragment on Organization {\n id\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n}\n\nfragment GeneralSettingsTabFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n createdAt\n updatedAt\n}\n\nfragment MembersSettingsTabInvitationsFragment on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n email\n role\n status\n createdAt\n expiresAt\n acceptedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MembersSettingsTabMembershipsFragment on Organization {\n memberships(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n authMethod\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SAMLSettingsTabFragment on Organization {\n id\n name\n samlConfigurations {\n id\n enabled\n emailDomain\n enforcementPolicy\n domainVerified\n domainVerificationToken\n domainVerifiedAt\n spEntityId\n spAcsUrl\n spMetadataUrl\n testLoginUrl\n idpEntityId\n idpSsoUrl\n idpCertificate\n idpMetadataUrl\n attributeEmail\n attributeFirstname\n attributeLastname\n attributeRole\n defaultRole\n autoSignupEnabled\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n ...GeneralSettingsTabFragment\n ...MembersSettingsTabMembershipsFragment\n ...MembersSettingsTabInvitationsFragment\n ...DomainSettingsTabFragment\n ...SAMLSettingsTabFragment\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "fda1489f2b80fd3d0b3962574bd7dfe3";
|
||||
(node as any).hash = "196e8c1fc9c2e0b3c76c8b338ed5c7f7";
|
||||
|
||||
export default node;
|
||||
|
||||
273
apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphCreateMutation.graphql.ts
generated
Normal file
273
apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphCreateMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* @generated SignedSource<<9232c5fa755e56b6a1ecdcdd51ad83db>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED";
|
||||
export type CreateSAMLConfigurationInput = {
|
||||
attributeEmail?: string | null | undefined;
|
||||
attributeFirstname?: string | null | undefined;
|
||||
attributeLastname?: string | null | undefined;
|
||||
attributeRole?: string | null | undefined;
|
||||
autoSignupEnabled?: boolean | null | undefined;
|
||||
defaultRole?: string | null | undefined;
|
||||
emailDomain: string;
|
||||
enforcementPolicy: SAMLEnforcementPolicy;
|
||||
idpCertificate?: string | null | undefined;
|
||||
idpEntityId?: string | null | undefined;
|
||||
idpMetadataUrl?: string | null | undefined;
|
||||
idpMetadataXml?: string | null | undefined;
|
||||
idpSsoUrl?: string | null | undefined;
|
||||
organizationId: string;
|
||||
spCertificate?: string | null | undefined;
|
||||
spPrivateKey?: string | null | undefined;
|
||||
};
|
||||
export type SAMLConfigurationGraphCreateMutation$variables = {
|
||||
input: CreateSAMLConfigurationInput;
|
||||
};
|
||||
export type SAMLConfigurationGraphCreateMutation$data = {
|
||||
readonly createSAMLConfiguration: {
|
||||
readonly samlConfiguration: {
|
||||
readonly attributeEmail: string;
|
||||
readonly attributeFirstname: string;
|
||||
readonly attributeLastname: string;
|
||||
readonly attributeRole: string;
|
||||
readonly autoSignupEnabled: boolean;
|
||||
readonly createdAt: any;
|
||||
readonly defaultRole: string;
|
||||
readonly emailDomain: string;
|
||||
readonly enabled: boolean;
|
||||
readonly enforcementPolicy: SAMLEnforcementPolicy;
|
||||
readonly id: string;
|
||||
readonly idpCertificate: string;
|
||||
readonly idpEntityId: string;
|
||||
readonly idpMetadataUrl: string | null | undefined;
|
||||
readonly idpSsoUrl: string;
|
||||
readonly spAcsUrl: string;
|
||||
readonly spEntityId: string;
|
||||
readonly spMetadataUrl: string;
|
||||
readonly testLoginUrl: string;
|
||||
readonly updatedAt: any;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type SAMLConfigurationGraphCreateMutation = {
|
||||
response: SAMLConfigurationGraphCreateMutation$data;
|
||||
variables: SAMLConfigurationGraphCreateMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "CreateSAMLConfigurationPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "createSAMLConfiguration",
|
||||
"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": "enabled",
|
||||
"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": "spEntityId",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "spAcsUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "spMetadataUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "testLoginUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpEntityId",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpSsoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpCertificate",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpMetadataUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeEmail",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeFirstname",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeLastname",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeRole",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "defaultRole",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "autoSignupEnabled",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SAMLConfigurationGraphCreateMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "SAMLConfigurationGraphCreateMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "71d5f80565cc8bc4cef0382c315b8cb1",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SAMLConfigurationGraphCreateMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SAMLConfigurationGraphCreateMutation(\n $input: CreateSAMLConfigurationInput!\n) {\n createSAMLConfiguration(input: $input) {\n samlConfiguration {\n id\n enabled\n emailDomain\n enforcementPolicy\n spEntityId\n spAcsUrl\n spMetadataUrl\n testLoginUrl\n idpEntityId\n idpSsoUrl\n idpCertificate\n idpMetadataUrl\n attributeEmail\n attributeFirstname\n attributeLastname\n attributeRole\n defaultRole\n autoSignupEnabled\n createdAt\n updatedAt\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "04237f5ad3588264149310798d6137f2";
|
||||
|
||||
export default node;
|
||||
92
apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphDeleteMutation.graphql.ts
generated
Normal file
92
apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphDeleteMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @generated SignedSource<<55ce4f4205b3eb01954423a75e6e9369>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type DeleteSAMLConfigurationInput = {
|
||||
id: string;
|
||||
};
|
||||
export type SAMLConfigurationGraphDeleteMutation$variables = {
|
||||
input: DeleteSAMLConfigurationInput;
|
||||
};
|
||||
export type SAMLConfigurationGraphDeleteMutation$data = {
|
||||
readonly deleteSAMLConfiguration: {
|
||||
readonly deletedSAMLConfigurationId: string;
|
||||
};
|
||||
};
|
||||
export type SAMLConfigurationGraphDeleteMutation = {
|
||||
response: SAMLConfigurationGraphDeleteMutation$data;
|
||||
variables: SAMLConfigurationGraphDeleteMutation$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": "SAMLConfigurationGraphDeleteMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "SAMLConfigurationGraphDeleteMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "be1192099e1b3d0765a358de10685870",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SAMLConfigurationGraphDeleteMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SAMLConfigurationGraphDeleteMutation(\n $input: DeleteSAMLConfigurationInput!\n) {\n deleteSAMLConfiguration(input: $input) {\n deletedSAMLConfigurationId\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "869072f879524c5c2acbc684f536bfe5";
|
||||
|
||||
export default node;
|
||||
113
apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphDisableMutation.graphql.ts
generated
Normal file
113
apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphDisableMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @generated SignedSource<<2101e8d4c307eb6c37e83c625ca7801f>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type DisableSAMLInput = {
|
||||
id: string;
|
||||
};
|
||||
export type SAMLConfigurationGraphDisableMutation$variables = {
|
||||
input: DisableSAMLInput;
|
||||
};
|
||||
export type SAMLConfigurationGraphDisableMutation$data = {
|
||||
readonly disableSAML: {
|
||||
readonly samlConfiguration: {
|
||||
readonly enabled: boolean;
|
||||
readonly id: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type SAMLConfigurationGraphDisableMutation = {
|
||||
response: SAMLConfigurationGraphDisableMutation$data;
|
||||
variables: SAMLConfigurationGraphDisableMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "DisableSAMLPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "disableSAML",
|
||||
"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": "enabled",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SAMLConfigurationGraphDisableMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "SAMLConfigurationGraphDisableMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "dbed05d465035d865dfd1f6473f88401",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SAMLConfigurationGraphDisableMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SAMLConfigurationGraphDisableMutation(\n $input: DisableSAMLInput!\n) {\n disableSAML(input: $input) {\n samlConfiguration {\n id\n enabled\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "6492fd7729c72ca805ef8f99d1399081";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<b883cd0523e827b2c71b89e1d249d6dd>>
|
||||
* @generated SignedSource<<8bcd8aad334a74db810b3d4c17fbb6ac>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -9,22 +9,23 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type AcceptInvitationInput = {
|
||||
invitationId: string;
|
||||
export type EnableSAMLInput = {
|
||||
id: string;
|
||||
};
|
||||
export type OrganizationsPage_AcceptInvitationMutation$variables = {
|
||||
input: AcceptInvitationInput;
|
||||
export type SAMLConfigurationGraphEnableMutation$variables = {
|
||||
input: EnableSAMLInput;
|
||||
};
|
||||
export type OrganizationsPage_AcceptInvitationMutation$data = {
|
||||
readonly acceptInvitation: {
|
||||
readonly invitation: {
|
||||
export type SAMLConfigurationGraphEnableMutation$data = {
|
||||
readonly enableSAML: {
|
||||
readonly samlConfiguration: {
|
||||
readonly enabled: boolean;
|
||||
readonly id: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type OrganizationsPage_AcceptInvitationMutation = {
|
||||
response: OrganizationsPage_AcceptInvitationMutation$data;
|
||||
variables: OrganizationsPage_AcceptInvitationMutation$variables;
|
||||
export type SAMLConfigurationGraphEnableMutation = {
|
||||
response: SAMLConfigurationGraphEnableMutation$data;
|
||||
variables: SAMLConfigurationGraphEnableMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -45,17 +46,17 @@ v1 = [
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "AcceptInvitationPayload",
|
||||
"concreteType": "EnableSAMLPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "acceptInvitation",
|
||||
"name": "enableSAML",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Invitation",
|
||||
"concreteType": "SAMLConfiguration",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitation",
|
||||
"name": "samlConfiguration",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
@@ -64,6 +65,13 @@ v1 = [
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "enabled",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -77,7 +85,7 @@ return {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "OrganizationsPage_AcceptInvitationMutation",
|
||||
"name": "SAMLConfigurationGraphEnableMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
@@ -86,20 +94,20 @@ return {
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "OrganizationsPage_AcceptInvitationMutation",
|
||||
"name": "SAMLConfigurationGraphEnableMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "cc4a442037edaa624948b5be0c009823",
|
||||
"cacheID": "2bd2356d146a9fa5f5a5f5480d310bc9",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "OrganizationsPage_AcceptInvitationMutation",
|
||||
"name": "SAMLConfigurationGraphEnableMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation OrganizationsPage_AcceptInvitationMutation(\n $input: AcceptInvitationInput!\n) {\n acceptInvitation(input: $input) {\n invitation {\n id\n }\n }\n}\n"
|
||||
"text": "mutation SAMLConfigurationGraphEnableMutation(\n $input: EnableSAMLInput!\n) {\n enableSAML(input: $input) {\n samlConfiguration {\n id\n enabled\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "190213ab6fdc068343a270b4fa94e160";
|
||||
(node as any).hash = "1627602d91776f718f1913011bdce786";
|
||||
|
||||
export default node;
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* @generated SignedSource<<55d58c6ee7e4dcd7ffe7e172aea932d5>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type InitiateDomainVerificationInput = {
|
||||
emailDomain: string;
|
||||
organizationId: string;
|
||||
};
|
||||
export type SAMLConfigurationGraphInitiateDomainVerificationMutation$variables = {
|
||||
input: InitiateDomainVerificationInput;
|
||||
};
|
||||
export type SAMLConfigurationGraphInitiateDomainVerificationMutation$data = {
|
||||
readonly initiateDomainVerification: {
|
||||
readonly dnsRecord: string;
|
||||
readonly samlConfiguration: {
|
||||
readonly domainVerificationToken: string | null | undefined;
|
||||
readonly domainVerified: boolean;
|
||||
readonly domainVerifiedAt: any | null | undefined;
|
||||
readonly emailDomain: string;
|
||||
readonly id: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type SAMLConfigurationGraphInitiateDomainVerificationMutation = {
|
||||
response: SAMLConfigurationGraphInitiateDomainVerificationMutation$data;
|
||||
variables: SAMLConfigurationGraphInitiateDomainVerificationMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "InitiateDomainVerificationPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "initiateDomainVerification",
|
||||
"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": "domainVerified",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "domainVerificationToken",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "domainVerifiedAt",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "dnsRecord",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SAMLConfigurationGraphInitiateDomainVerificationMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "SAMLConfigurationGraphInitiateDomainVerificationMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "61453ca7fb2c7ff2fe8b657f6cfb7497",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SAMLConfigurationGraphInitiateDomainVerificationMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SAMLConfigurationGraphInitiateDomainVerificationMutation(\n $input: InitiateDomainVerificationInput!\n) {\n initiateDomainVerification(input: $input) {\n samlConfiguration {\n id\n emailDomain\n domainVerified\n domainVerificationToken\n domainVerifiedAt\n }\n dnsRecord\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "5424f4fd94618327ae64292d31349e73";
|
||||
|
||||
export default node;
|
||||
272
apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphUpdateMutation.graphql.ts
generated
Normal file
272
apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphUpdateMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* @generated SignedSource<<694befcb248cbb23e66b9555626f223b>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED";
|
||||
export type UpdateSAMLConfigurationInput = {
|
||||
attributeEmail?: string | null | undefined;
|
||||
attributeFirstname?: string | null | undefined;
|
||||
attributeLastname?: string | null | undefined;
|
||||
attributeRole?: string | null | undefined;
|
||||
autoSignupEnabled?: boolean | null | undefined;
|
||||
defaultRole?: string | null | undefined;
|
||||
enabled?: boolean | null | undefined;
|
||||
enforcementPolicy?: SAMLEnforcementPolicy | null | undefined;
|
||||
id: string;
|
||||
idpCertificate?: string | null | undefined;
|
||||
idpEntityId?: string | null | undefined;
|
||||
idpMetadataUrl?: string | null | undefined;
|
||||
idpSsoUrl?: string | null | undefined;
|
||||
spCertificate?: string | null | undefined;
|
||||
spPrivateKey?: string | null | undefined;
|
||||
};
|
||||
export type SAMLConfigurationGraphUpdateMutation$variables = {
|
||||
input: UpdateSAMLConfigurationInput;
|
||||
};
|
||||
export type SAMLConfigurationGraphUpdateMutation$data = {
|
||||
readonly updateSAMLConfiguration: {
|
||||
readonly samlConfiguration: {
|
||||
readonly attributeEmail: string;
|
||||
readonly attributeFirstname: string;
|
||||
readonly attributeLastname: string;
|
||||
readonly attributeRole: string;
|
||||
readonly autoSignupEnabled: boolean;
|
||||
readonly createdAt: any;
|
||||
readonly defaultRole: string;
|
||||
readonly emailDomain: string;
|
||||
readonly enabled: boolean;
|
||||
readonly enforcementPolicy: SAMLEnforcementPolicy;
|
||||
readonly id: string;
|
||||
readonly idpCertificate: string;
|
||||
readonly idpEntityId: string;
|
||||
readonly idpMetadataUrl: string | null | undefined;
|
||||
readonly idpSsoUrl: string;
|
||||
readonly spAcsUrl: string;
|
||||
readonly spEntityId: string;
|
||||
readonly spMetadataUrl: string;
|
||||
readonly testLoginUrl: string;
|
||||
readonly updatedAt: any;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type SAMLConfigurationGraphUpdateMutation = {
|
||||
response: SAMLConfigurationGraphUpdateMutation$data;
|
||||
variables: SAMLConfigurationGraphUpdateMutation$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": "enabled",
|
||||
"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": "spEntityId",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "spAcsUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "spMetadataUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "testLoginUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpEntityId",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpSsoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpCertificate",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpMetadataUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeEmail",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeFirstname",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeLastname",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeRole",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "defaultRole",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "autoSignupEnabled",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SAMLConfigurationGraphUpdateMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "SAMLConfigurationGraphUpdateMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "c9e4055888055b109aa71b665c9544ca",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SAMLConfigurationGraphUpdateMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SAMLConfigurationGraphUpdateMutation(\n $input: UpdateSAMLConfigurationInput!\n) {\n updateSAMLConfiguration(input: $input) {\n samlConfiguration {\n id\n enabled\n emailDomain\n enforcementPolicy\n spEntityId\n spAcsUrl\n spMetadataUrl\n testLoginUrl\n idpEntityId\n idpSsoUrl\n idpCertificate\n idpMetadataUrl\n attributeEmail\n attributeFirstname\n attributeLastname\n attributeRole\n defaultRole\n autoSignupEnabled\n createdAt\n updatedAt\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "d572af05811d6a05cdd4446fe175894d";
|
||||
|
||||
export default node;
|
||||
129
apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphVerifyDomainMutation.graphql.ts
generated
Normal file
129
apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphVerifyDomainMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @generated SignedSource<<8baf2363740e7eb47e7654f2f55c09df>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type VerifyDomainInput = {
|
||||
id: string;
|
||||
};
|
||||
export type SAMLConfigurationGraphVerifyDomainMutation$variables = {
|
||||
input: VerifyDomainInput;
|
||||
};
|
||||
export type SAMLConfigurationGraphVerifyDomainMutation$data = {
|
||||
readonly verifyDomain: {
|
||||
readonly samlConfiguration: {
|
||||
readonly domainVerified: boolean;
|
||||
readonly domainVerifiedAt: any | null | undefined;
|
||||
readonly id: string;
|
||||
};
|
||||
readonly verified: boolean;
|
||||
};
|
||||
};
|
||||
export type SAMLConfigurationGraphVerifyDomainMutation = {
|
||||
response: SAMLConfigurationGraphVerifyDomainMutation$data;
|
||||
variables: SAMLConfigurationGraphVerifyDomainMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "VerifyDomainPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "verifyDomain",
|
||||
"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": "domainVerified",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "domainVerifiedAt",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "verified",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SAMLConfigurationGraphVerifyDomainMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "SAMLConfigurationGraphVerifyDomainMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "1f241529f518d2f0701d9c4f7a731d6b",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SAMLConfigurationGraphVerifyDomainMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SAMLConfigurationGraphVerifyDomainMutation(\n $input: VerifyDomainInput!\n) {\n verifyDomain(input: $input) {\n samlConfiguration {\n id\n domainVerified\n domainVerifiedAt\n }\n verified\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "ea3f1fe691b0c36ff7e54663479f7e7c";
|
||||
|
||||
export default node;
|
||||
@@ -30,17 +30,16 @@ import {
|
||||
DropdownItem,
|
||||
IconChevronGrabberVertical,
|
||||
IconPlusLarge,
|
||||
IconChevronDown,
|
||||
Avatar,
|
||||
IconPeopleAdd,
|
||||
Badge,
|
||||
IconLock,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
||||
import { useLazyLoadQuery } from "react-relay";
|
||||
import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql";
|
||||
import type { MainLayout_OrganizationSelector_viewer$key } from "./__generated__/MainLayout_OrganizationSelector_viewer.graphql";
|
||||
import { Suspense, useState } from "react";
|
||||
import { Suspense, useState, useEffect } from "react";
|
||||
import { useToast } from "@probo/ui";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { PageError } from "/components/PageError";
|
||||
@@ -54,7 +53,9 @@ const MainLayoutQuery = graphql`
|
||||
fullName
|
||||
email
|
||||
}
|
||||
...MainLayout_OrganizationSelector_viewer
|
||||
invitations(first: 1, filter: {statuses: [PENDING]}) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
organization: node(id: $organizationId) {
|
||||
... on Organization {
|
||||
@@ -66,33 +67,6 @@ const MainLayoutQuery = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const OrganizationSelectorFragment = graphql`
|
||||
fragment MainLayout_OrganizationSelector_viewer on Viewer
|
||||
@refetchable(queryName: "MainLayoutOrganizationSelectorPaginationQuery")
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 25 }
|
||||
after: { type: "CursorKey" }
|
||||
) {
|
||||
organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC})
|
||||
@connection(key: "MainLayout_OrganizationSelector_organizations") {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
invitations(first: 1, filter: {statuses: [PENDING]}) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Site layout with a header and a sidebar
|
||||
*/
|
||||
@@ -228,7 +202,7 @@ function UserDropdown({ organizationId }: { organizationId: string }) {
|
||||
) => {
|
||||
e.preventDefault();
|
||||
|
||||
fetch(buildEndpoint("/api/console/v1/auth/logout"), {
|
||||
fetch(buildEndpoint("/auth/logout"), {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -271,6 +245,19 @@ function UserDropdown({ organizationId }: { organizationId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
interface Organization {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string | null;
|
||||
authenticationMethod: string;
|
||||
authStatus: "authenticated" | "unauthenticated" | "expired";
|
||||
loginUrl: string;
|
||||
}
|
||||
|
||||
interface OrganizationsResponse {
|
||||
organizations: Organization[];
|
||||
}
|
||||
|
||||
function OrganizationSelectorWrapper({ organizationId }: { organizationId: string }) {
|
||||
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, { organizationId });
|
||||
return <OrganizationSelector viewer={data.viewer} currentOrganization={data.organization} />;
|
||||
@@ -280,31 +267,55 @@ function OrganizationSelector({
|
||||
viewer,
|
||||
currentOrganization
|
||||
}: {
|
||||
viewer: MainLayout_OrganizationSelector_viewer$key;
|
||||
viewer: MainLayoutQueryType["response"]["viewer"];
|
||||
currentOrganization: MainLayoutQueryType["response"]["organization"];
|
||||
}) {
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { __ } = useTranslate();
|
||||
|
||||
const { data, loadNext, hasNext } = usePaginationFragment(
|
||||
OrganizationSelectorFragment,
|
||||
viewer
|
||||
);
|
||||
const pendingInvitationsCount = viewer.invitations.totalCount;
|
||||
|
||||
const organizations = data.organizations.edges.map((edge) => edge.node);
|
||||
const pendingInvitationsCount = data.invitations.totalCount;
|
||||
useEffect(() => {
|
||||
const fetchOrganizations = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch('/auth/organizations', {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const handleLoadMore = (e?: React.MouseEvent) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch organizations');
|
||||
}
|
||||
|
||||
if (hasNext && !isLoadingMore) {
|
||||
setIsLoadingMore(true);
|
||||
loadNext(25, {
|
||||
onComplete: () => setIsLoadingMore(false),
|
||||
});
|
||||
}
|
||||
};
|
||||
const data: OrganizationsResponse = await response.json();
|
||||
setOrganizations(data.organizations);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
console.error('Failed to fetch organizations:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchOrganizations();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
className="-ml-3"
|
||||
variant="tertiary"
|
||||
disabled
|
||||
>
|
||||
{__("Error loading organizations")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -314,39 +325,70 @@ function OrganizationSelector({
|
||||
className="-ml-3"
|
||||
variant="tertiary"
|
||||
iconAfter={IconChevronGrabberVertical}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{currentOrganization?.name || ""}
|
||||
{isLoading ? __("Loading...") : (currentOrganization?.name || "")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
|
||||
{organizations.map((organization) => (
|
||||
<DropdownItem
|
||||
asChild
|
||||
key={organization.id}
|
||||
>
|
||||
<Link to={`/organizations/${organization.id}`}>
|
||||
<Avatar src={organization.logoUrl} name={organization.name} />
|
||||
{organization.name}
|
||||
</Link>
|
||||
</DropdownItem>
|
||||
))}
|
||||
{hasNext && (
|
||||
<div className="px-3 py-1 flex justify-center">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={handleLoadMore}
|
||||
onMouseDown={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="mx-auto"
|
||||
icon={IconChevronDown}
|
||||
disabled={isLoadingMore}
|
||||
>
|
||||
{isLoadingMore ? __("Loading...") : __("Show More")}
|
||||
</Button>
|
||||
{isLoading ? (
|
||||
<div className="px-3 py-2 text-gray-500">
|
||||
{__("Loading organizations...")}
|
||||
</div>
|
||||
) : organizations.length === 0 ? (
|
||||
<div className="px-3 py-2 text-gray-500">
|
||||
{__("No organizations found")}
|
||||
</div>
|
||||
) : (
|
||||
organizations.map((organization) => {
|
||||
const isAuthenticated = organization.authStatus === "authenticated";
|
||||
const isExpired = organization.authStatus === "expired";
|
||||
const needsAuth = organization.authStatus === "unauthenticated";
|
||||
|
||||
const targetUrl = isAuthenticated
|
||||
? `/organizations/${organization.id}`
|
||||
: organization.loginUrl;
|
||||
|
||||
const isSAMLUrl = targetUrl.includes('/auth/saml/');
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
asChild
|
||||
key={organization.id}
|
||||
>
|
||||
{isSAMLUrl ? (
|
||||
<a href={targetUrl} className="flex items-center gap-2">
|
||||
<Avatar name={organization.name} src={organization.logoUrl} />
|
||||
<span className="flex-1">{organization.name}</span>
|
||||
{isAuthenticated && (
|
||||
<IconCheckmark1 size={16} className="text-green-600" />
|
||||
)}
|
||||
{isExpired && (
|
||||
<IconClock size={16} className="text-orange-600" />
|
||||
)}
|
||||
{needsAuth && (
|
||||
<IconLock size={16} className="text-gray-400" />
|
||||
)}
|
||||
</a>
|
||||
) : (
|
||||
<Link to={targetUrl} className="flex items-center gap-2">
|
||||
<Avatar name={organization.name} src={organization.logoUrl} />
|
||||
<span className="flex-1">{organization.name}</span>
|
||||
{isAuthenticated && (
|
||||
<IconCheckmark1 size={16} className="text-green-600" />
|
||||
)}
|
||||
{isExpired && (
|
||||
<IconClock size={16} className="text-orange-600" />
|
||||
)}
|
||||
{needsAuth && (
|
||||
<IconLock size={16} className="text-gray-400" />
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</DropdownItem>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<DropdownSeparator />
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
/**
|
||||
* @generated SignedSource<<3a99643b92330af6920aac4c2286376f>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type MainLayoutOrganizationSelectorPaginationQuery$variables = {
|
||||
after?: any | null | undefined;
|
||||
first?: number | null | undefined;
|
||||
};
|
||||
export type MainLayoutOrganizationSelectorPaginationQuery$data = {
|
||||
readonly viewer: {
|
||||
readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">;
|
||||
};
|
||||
};
|
||||
export type MainLayoutOrganizationSelectorPaginationQuery = {
|
||||
response: MainLayoutOrganizationSelectorPaginationQuery$data;
|
||||
variables: MainLayoutOrganizationSelectorPaginationQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "after"
|
||||
},
|
||||
{
|
||||
"defaultValue": 25,
|
||||
"kind": "LocalArgument",
|
||||
"name": "first"
|
||||
}
|
||||
],
|
||||
v1 = {
|
||||
"kind": "Variable",
|
||||
"name": "after",
|
||||
"variableName": "after"
|
||||
},
|
||||
v2 = {
|
||||
"kind": "Variable",
|
||||
"name": "first",
|
||||
"variableName": "first"
|
||||
},
|
||||
v3 = [
|
||||
(v1/*: any*/),
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "ASC",
|
||||
"field": "NAME"
|
||||
}
|
||||
}
|
||||
],
|
||||
v4 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "MainLayoutOrganizationSelectorPaginationQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"args": [
|
||||
(v1/*: any*/),
|
||||
(v2/*: any*/)
|
||||
],
|
||||
"kind": "FragmentSpread",
|
||||
"name": "MainLayout_OrganizationSelector_viewer"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "MainLayoutOrganizationSelectorPaginationQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v3/*: any*/),
|
||||
"concreteType": "OrganizationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "organizations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "OrganizationEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v4/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
"kind": "LinkedField",
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v3/*: any*/),
|
||||
"filters": [
|
||||
"orderBy"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "MainLayout_OrganizationSelector_organizations",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "organizations"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"statuses": [
|
||||
"PENDING"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "totalCount",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)"
|
||||
},
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "d51b49a50afe664ee9903c1bb265ab79",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "MainLayoutOrganizationSelectorPaginationQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "3e00f1a6f8089fc59144807a07fd1bdf";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<1a7aa899a9b6251477122c87ae1c6431>>
|
||||
* @generated SignedSource<<26b3620e6aed7f97ffb1710be1eb267a>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -9,7 +9,6 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type MainLayoutQuery$variables = {
|
||||
organizationId: string;
|
||||
};
|
||||
@@ -21,11 +20,13 @@ export type MainLayoutQuery$data = {
|
||||
};
|
||||
readonly viewer: {
|
||||
readonly id: string;
|
||||
readonly invitations: {
|
||||
readonly totalCount: number;
|
||||
};
|
||||
readonly user: {
|
||||
readonly email: string;
|
||||
readonly fullName: string;
|
||||
};
|
||||
readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">;
|
||||
};
|
||||
};
|
||||
export type MainLayoutQuery = {
|
||||
@@ -62,48 +63,59 @@ v3 = {
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
},
|
||||
v4 = [
|
||||
v4 = {
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"statuses": [
|
||||
"PENDING"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "totalCount",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)"
|
||||
},
|
||||
v5 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "id",
|
||||
"variableName": "organizationId"
|
||||
}
|
||||
],
|
||||
v5 = {
|
||||
v6 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
v6 = {
|
||||
v7 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
v7 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 25
|
||||
},
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "ASC",
|
||||
"field": "NAME"
|
||||
}
|
||||
}
|
||||
],
|
||||
v8 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
@@ -134,17 +146,13 @@ return {
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "MainLayout_OrganizationSelector_viewer"
|
||||
}
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": "organization",
|
||||
"args": (v4/*: any*/),
|
||||
"args": (v5/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
@@ -154,8 +162,8 @@ return {
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/)
|
||||
(v6/*: any*/),
|
||||
(v7/*: any*/)
|
||||
],
|
||||
"type": "Organization",
|
||||
"abstractKey": null
|
||||
@@ -196,137 +204,31 @@ return {
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v7/*: any*/),
|
||||
"concreteType": "OrganizationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "organizations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "OrganizationEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/),
|
||||
(v8/*: 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": "hasNextPage",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "organizations(first:25,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v7/*: any*/),
|
||||
"filters": [
|
||||
"orderBy"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "MainLayout_OrganizationSelector_organizations",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "organizations"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"statuses": [
|
||||
"PENDING"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "totalCount",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)"
|
||||
}
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": "organization",
|
||||
"args": (v4/*: any*/),
|
||||
"args": (v5/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v8/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
(v1/*: any*/),
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/)
|
||||
(v6/*: any*/),
|
||||
(v7/*: any*/)
|
||||
],
|
||||
"type": "Organization",
|
||||
"abstractKey": null
|
||||
@@ -337,16 +239,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "6de25e54419d82cb6465c903a0afdd78",
|
||||
"cacheID": "a8f9f58d27677c55b5a217617db83e27",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "MainLayoutQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n}\n"
|
||||
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "aaaca58a896839aa85ce7c2fcf75de39";
|
||||
(node as any).hash = "17986fcea321c4567d86584d1a9f89c1";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
/**
|
||||
* @generated SignedSource<<b0e167541047d61efc6d30382c062d0d>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type MainLayout_OrganizationSelector_viewer$data = {
|
||||
readonly invitations: {
|
||||
readonly totalCount: number;
|
||||
};
|
||||
readonly organizations: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly id: string;
|
||||
readonly logoUrl: string | null | undefined;
|
||||
readonly name: string;
|
||||
};
|
||||
}>;
|
||||
readonly pageInfo: {
|
||||
readonly endCursor: any | null | undefined;
|
||||
readonly hasNextPage: boolean;
|
||||
};
|
||||
};
|
||||
readonly " $fragmentType": "MainLayout_OrganizationSelector_viewer";
|
||||
};
|
||||
export type MainLayout_OrganizationSelector_viewer$key = {
|
||||
readonly " $data"?: MainLayout_OrganizationSelector_viewer$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">;
|
||||
};
|
||||
|
||||
import MainLayoutOrganizationSelectorPaginationQuery_graphql from './MainLayoutOrganizationSelectorPaginationQuery.graphql';
|
||||
|
||||
const node: ReaderFragment = (function(){
|
||||
var v0 = [
|
||||
"organizations"
|
||||
];
|
||||
return {
|
||||
"argumentDefinitions": [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "after"
|
||||
},
|
||||
{
|
||||
"defaultValue": 25,
|
||||
"kind": "LocalArgument",
|
||||
"name": "first"
|
||||
}
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": {
|
||||
"connection": [
|
||||
{
|
||||
"count": "first",
|
||||
"cursor": "after",
|
||||
"direction": "forward",
|
||||
"path": (v0/*: any*/)
|
||||
}
|
||||
],
|
||||
"refetch": {
|
||||
"connection": {
|
||||
"forward": {
|
||||
"count": "first",
|
||||
"cursor": "after"
|
||||
},
|
||||
"backward": null,
|
||||
"path": (v0/*: any*/)
|
||||
},
|
||||
"fragmentPathInResult": [
|
||||
"viewer"
|
||||
],
|
||||
"operation": MainLayoutOrganizationSelectorPaginationQuery_graphql
|
||||
}
|
||||
},
|
||||
"name": "MainLayout_OrganizationSelector_viewer",
|
||||
"selections": [
|
||||
{
|
||||
"alias": "organizations",
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "ASC",
|
||||
"field": "NAME"
|
||||
}
|
||||
}
|
||||
],
|
||||
"concreteType": "OrganizationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "__MainLayout_OrganizationSelector_organizations_connection",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "OrganizationEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
"kind": "LinkedField",
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "__MainLayout_OrganizationSelector_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"statuses": [
|
||||
"PENDING"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "totalCount",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)"
|
||||
}
|
||||
],
|
||||
"type": "Viewer",
|
||||
"abstractKey": null
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "3e00f1a6f8089fc59144807a07fd1bdf";
|
||||
|
||||
export default node;
|
||||
@@ -1,110 +1,138 @@
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useLazyLoadQuery } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import type { OrganizationsPageQuery as OrganizationsPageQueryType } from "./__generated__/OrganizationsPageQuery.graphql";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
IconPlusLarge,
|
||||
IconCheckmark1,
|
||||
IconLock,
|
||||
IconClock,
|
||||
Badge,
|
||||
} from "@probo/ui";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { formatDate } from "@probo/helpers";
|
||||
|
||||
const OrganizationsPageQuery = graphql`
|
||||
query OrganizationsPageQuery {
|
||||
viewer {
|
||||
organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) @connection(key: "OrganizationsPage_organizations") {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {statuses: [PENDING]}) @connection(key: "OrganizationsPage_invitations") {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
email
|
||||
fullName
|
||||
role
|
||||
expiresAt
|
||||
acceptedAt
|
||||
createdAt
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
interface Organization {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string | null;
|
||||
authenticationMethod: string;
|
||||
authStatus: "authenticated" | "unauthenticated" | "expired";
|
||||
loginUrl: string;
|
||||
}
|
||||
|
||||
const acceptInvitationMutation = graphql`
|
||||
mutation OrganizationsPage_AcceptInvitationMutation($input: AcceptInvitationInput!) {
|
||||
acceptInvitation(input: $input) {
|
||||
invitation {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
interface Invitation {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: string;
|
||||
expiresAt: string;
|
||||
acceptedAt?: string | null;
|
||||
createdAt: string;
|
||||
organization: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function OrganizationsPage() {
|
||||
const { __ } = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
const data = useLazyLoadQuery<OrganizationsPageQueryType>(
|
||||
OrganizationsPageQuery,
|
||||
{}
|
||||
);
|
||||
|
||||
const organizations = data.viewer.organizations.edges.map(
|
||||
(edge) => edge.node
|
||||
);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(true);
|
||||
const [invitations, setInvitations] = useState<Invitation[]>([]);
|
||||
const [isLoadingInvitations, setIsLoadingInvitations] = useState(true);
|
||||
const [isAccepting, setIsAccepting] = useState(false);
|
||||
|
||||
const pendingInvitations = data.viewer.invitations.edges.map(
|
||||
(edge) => edge.node
|
||||
);
|
||||
// Fetch organizations from REST endpoint
|
||||
useEffect(() => {
|
||||
const fetchOrganizations = async () => {
|
||||
try {
|
||||
const response = await fetch('/auth/organizations', {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const [acceptInvitation, isAccepting] = useMutationWithToasts(
|
||||
acceptInvitationMutation,
|
||||
{
|
||||
successMessage: __("Invitation accepted successfully"),
|
||||
errorMessage: __("Failed to accept invitation"),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch organizations');
|
||||
}
|
||||
|
||||
const handleAcceptInvitation = (invitationId: string, organizationId: string) => {
|
||||
acceptInvitation({
|
||||
variables: {
|
||||
input: {
|
||||
invitationId,
|
||||
const data: { organizations: Organization[] } = await response.json();
|
||||
setOrganizations(data.organizations);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch organizations:', err);
|
||||
} finally {
|
||||
setIsLoadingOrganizations(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchOrganizations();
|
||||
}, []);
|
||||
|
||||
// Fetch pending invitations from REST endpoint
|
||||
useEffect(() => {
|
||||
const fetchInvitations = async () => {
|
||||
try {
|
||||
const response = await fetch('/auth/invitations', {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch invitations');
|
||||
}
|
||||
|
||||
const data: { invitations: Invitation[] } = await response.json();
|
||||
setInvitations(data.invitations);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch invitations:', err);
|
||||
} finally {
|
||||
setIsLoadingInvitations(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchInvitations();
|
||||
}, []);
|
||||
|
||||
const handleAcceptInvitation = async (invitationId: string, organizationId: string) => {
|
||||
setIsAccepting(true);
|
||||
try {
|
||||
const response = await fetch('/auth/invitations/accept', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate(`/organizations/${organizationId}`);
|
||||
},
|
||||
});
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ invitationId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to accept invitation');
|
||||
}
|
||||
|
||||
// Navigate to the organization after successful acceptance
|
||||
navigate(`/organizations/${organizationId}`);
|
||||
} catch (err) {
|
||||
console.error('Failed to accept invitation:', err);
|
||||
alert(__('Failed to accept invitation'));
|
||||
} finally {
|
||||
setIsAccepting(false);
|
||||
}
|
||||
};
|
||||
|
||||
usePageTitle(__("Select an organization"));
|
||||
|
||||
useEffect(() => {
|
||||
if (organizations.length === 1 && pendingInvitations.length === 0) {
|
||||
navigate(`/organizations/${organizations[0].id}`);
|
||||
} else if (organizations.length === 0 && pendingInvitations.length === 0) {
|
||||
navigate("/organizations/new");
|
||||
// Only auto-navigate once both organizations and invitations are loaded
|
||||
if (!isLoadingOrganizations && !isLoadingInvitations) {
|
||||
if (organizations.length === 1 && invitations.length === 0) {
|
||||
navigate(`/organizations/${organizations[0].id}`);
|
||||
} else if (organizations.length === 0 && invitations.length === 0) {
|
||||
navigate("/organizations/new");
|
||||
}
|
||||
}
|
||||
}, [organizations, pendingInvitations]);
|
||||
}, [organizations, invitations, isLoadingOrganizations, isLoadingInvitations, navigate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -113,12 +141,12 @@ export default function OrganizationsPage() {
|
||||
{__("Select an organization")}
|
||||
</h1>
|
||||
<div className="space-y-4 w-full">
|
||||
{pendingInvitations.length > 0 && (
|
||||
{invitations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{__("Pending invitations")}
|
||||
</h2>
|
||||
{pendingInvitations.map((invitation) => (
|
||||
{invitations.map((invitation) => (
|
||||
<InvitationCard
|
||||
key={invitation.id}
|
||||
invitation={invitation}
|
||||
@@ -130,7 +158,7 @@ export default function OrganizationsPage() {
|
||||
)}
|
||||
{organizations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{pendingInvitations.length > 0 && (
|
||||
{invitations.length > 0 && (
|
||||
<h2 className="text-xl font-semibold">
|
||||
{__("Your organizations")}
|
||||
</h2>
|
||||
@@ -166,18 +194,7 @@ export default function OrganizationsPage() {
|
||||
}
|
||||
|
||||
type InvitationCardProps = {
|
||||
invitation: {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: string;
|
||||
expiresAt: string;
|
||||
createdAt: string;
|
||||
organization: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
invitation: Invitation;
|
||||
onAccept: (invitationId: string, organizationId: string) => void;
|
||||
isAccepting: boolean;
|
||||
};
|
||||
@@ -211,35 +228,106 @@ function InvitationCard({ invitation, onAccept, isAccepting }: InvitationCardPro
|
||||
}
|
||||
|
||||
type OrganizationCardProps = {
|
||||
organization: {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl: string | null | undefined;
|
||||
};
|
||||
organization: Organization;
|
||||
};
|
||||
|
||||
function OrganizationCard({ organization }: OrganizationCardProps) {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
const isAuthenticated = organization.authStatus === "authenticated";
|
||||
const isExpired = organization.authStatus === "expired";
|
||||
const needsAuth = organization.authStatus === "unauthenticated";
|
||||
|
||||
// Determine target URL and button text based on auth status
|
||||
const targetUrl = isAuthenticated
|
||||
? `/organizations/${organization.id}`
|
||||
: organization.loginUrl;
|
||||
|
||||
const getAuthBadge = () => {
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<Badge variant="success" className="flex items-center gap-1">
|
||||
<IconCheckmark1 size={14} />
|
||||
{__("Authenticated")}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (isExpired) {
|
||||
return (
|
||||
<Badge variant="warning" className="flex items-center gap-1">
|
||||
<IconClock size={14} />
|
||||
{__("Session expired")}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (needsAuth) {
|
||||
return (
|
||||
<Badge variant="neutral" className="flex items-center gap-1">
|
||||
<IconLock size={14} />
|
||||
{__("Authentication required")}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getButtonText = () => {
|
||||
if (isAuthenticated) return __("Select");
|
||||
if (organization.authenticationMethod === "saml") return __("Login with SAML");
|
||||
return __("Login");
|
||||
};
|
||||
|
||||
// Check if the URL is a backend SAML endpoint
|
||||
const isSAMLUrl = targetUrl.includes('/auth/saml/');
|
||||
|
||||
return (
|
||||
<Card padded className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
to={`/organizations/${organization.id}`}
|
||||
className="flex items-center gap-4 hover:text-primary flex-1"
|
||||
>
|
||||
<Avatar
|
||||
src={organization.logoUrl}
|
||||
name={organization.name}
|
||||
size="l"
|
||||
/>
|
||||
<h2 className="font-semibold text-xl">{organization.name}</h2>
|
||||
</Link>
|
||||
{isSAMLUrl ? (
|
||||
<a
|
||||
href={targetUrl}
|
||||
className="flex items-center gap-4 hover:text-primary flex-1"
|
||||
>
|
||||
<Avatar
|
||||
src={organization.logoUrl}
|
||||
name={organization.name}
|
||||
size="l"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="font-semibold text-xl">{organization.name}</h2>
|
||||
{getAuthBadge()}
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
to={targetUrl}
|
||||
className="flex items-center gap-4 hover:text-primary flex-1"
|
||||
>
|
||||
<Avatar
|
||||
src={organization.logoUrl}
|
||||
name={organization.name}
|
||||
size="l"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="font-semibold text-xl">{organization.name}</h2>
|
||||
{getAuthBadge()}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button asChild>
|
||||
<Link to={`/organizations/${organization.id}`}>
|
||||
{__("Select")}
|
||||
</Link>
|
||||
{isSAMLUrl ? (
|
||||
<a href={targetUrl}>
|
||||
{getButtonText()}
|
||||
</a>
|
||||
) : (
|
||||
<Link to={targetUrl}>
|
||||
{getButtonText()}
|
||||
</Link>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,420 +0,0 @@
|
||||
/**
|
||||
* @generated SignedSource<<580387b50cff64395ba9afd4c7454d20>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type OrganizationsPageQuery$variables = Record<PropertyKey, never>;
|
||||
export type OrganizationsPageQuery$data = {
|
||||
readonly viewer: {
|
||||
readonly invitations: {
|
||||
readonly __id: string;
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly acceptedAt: any | null | undefined;
|
||||
readonly createdAt: any;
|
||||
readonly email: string;
|
||||
readonly expiresAt: any;
|
||||
readonly fullName: string;
|
||||
readonly id: string;
|
||||
readonly organization: {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
};
|
||||
readonly role: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
readonly organizations: {
|
||||
readonly __id: string;
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly id: string;
|
||||
readonly logoUrl: string | null | undefined;
|
||||
readonly name: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type OrganizationsPageQuery = {
|
||||
response: OrganizationsPageQuery$data;
|
||||
variables: OrganizationsPageQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = {
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "ASC",
|
||||
"field": "NAME"
|
||||
}
|
||||
},
|
||||
v1 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
v4 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
},
|
||||
v5 = {
|
||||
"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
|
||||
},
|
||||
v6 = {
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__id",
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
v7 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "OrganizationEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/)
|
||||
],
|
||||
v8 = {
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"statuses": [
|
||||
"PENDING"
|
||||
]
|
||||
}
|
||||
},
|
||||
v9 = {
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "DESC",
|
||||
"field": "CREATED_AT"
|
||||
}
|
||||
},
|
||||
v10 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "InvitationEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Invitation",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "role",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "expiresAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "acceptedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organization",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
(v2/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/)
|
||||
],
|
||||
v11 = {
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1000
|
||||
},
|
||||
v12 = [
|
||||
(v11/*: any*/),
|
||||
(v0/*: any*/)
|
||||
],
|
||||
v13 = [
|
||||
(v8/*: any*/),
|
||||
(v11/*: any*/),
|
||||
(v9/*: any*/)
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "OrganizationsPageQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": "organizations",
|
||||
"args": [
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"concreteType": "OrganizationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "__OrganizationsPage_organizations_connection",
|
||||
"plural": false,
|
||||
"selections": (v7/*: any*/),
|
||||
"storageKey": "__OrganizationsPage_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||
},
|
||||
{
|
||||
"alias": "invitations",
|
||||
"args": [
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/)
|
||||
],
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "__OrganizationsPage_invitations_connection",
|
||||
"plural": false,
|
||||
"selections": (v10/*: any*/),
|
||||
"storageKey": "__OrganizationsPage_invitations_connection(filter:{\"statuses\":[\"PENDING\"]},orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Operation",
|
||||
"name": "OrganizationsPageQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v12/*: any*/),
|
||||
"concreteType": "OrganizationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "organizations",
|
||||
"plural": false,
|
||||
"selections": (v7/*: any*/),
|
||||
"storageKey": "organizations(first:1000,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v12/*: any*/),
|
||||
"filters": [
|
||||
"orderBy"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "OrganizationsPage_organizations",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "organizations"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v13/*: any*/),
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": (v10/*: any*/),
|
||||
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v13/*: any*/),
|
||||
"filters": [
|
||||
"orderBy",
|
||||
"filter"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "OrganizationsPage_invitations",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "invitations"
|
||||
},
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "1b2e528133e56da643b9f8214a2f7192",
|
||||
"id": null,
|
||||
"metadata": {
|
||||
"connection": [
|
||||
{
|
||||
"count": null,
|
||||
"cursor": null,
|
||||
"direction": "forward",
|
||||
"path": [
|
||||
"viewer",
|
||||
"organizations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"count": null,
|
||||
"cursor": null,
|
||||
"direction": "forward",
|
||||
"path": [
|
||||
"viewer",
|
||||
"invitations"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "OrganizationsPageQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {statuses: [PENDING]}) {\n edges {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n organization {\n id\n name\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "ecd2534f36aa95ff7ff864ff22f09189";
|
||||
|
||||
export default node;
|
||||
@@ -111,7 +111,7 @@ export default function ConfirmEmailPage() {
|
||||
<p className="text-green-600 dark:text-green-400">
|
||||
{__("Your email has been confirmed successfully!")}
|
||||
</p>
|
||||
<Button onClick={() => navigate("/auth/login")} className="w-full">
|
||||
<Button onClick={() => navigate("/authentication/login")} className="w-full">
|
||||
{__("Proceed to Login")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -141,7 +141,7 @@ export default function ConfirmEmailPage() {
|
||||
{!isConfirmed && (
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
<Link
|
||||
to="/auth/login"
|
||||
to="/authentication/login"
|
||||
className="underline text-txt-primary hover:text-txt-secondary"
|
||||
>
|
||||
{__("Back to Login")}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function ForgotPasswordPage() {
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
const response = await fetch(
|
||||
buildEndpoint("/api/console/v1/auth/forget-password"),
|
||||
buildEndpoint("/auth/forget-password"),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -81,7 +81,7 @@ export default function ForgotPasswordPage() {
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{__("Remember your password?")}{" "}
|
||||
<Link
|
||||
to="/auth/login"
|
||||
to="/authentication/login"
|
||||
className="underline text-txt-primary hover:text-txt-secondary"
|
||||
>
|
||||
{__("Back to login")}
|
||||
@@ -124,7 +124,7 @@ export default function ForgotPasswordPage() {
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{__("Remember your password?")}{" "}
|
||||
<Link
|
||||
to="/auth/login"
|
||||
to="/authentication/login"
|
||||
className="underline text-txt-primary hover:text-txt-secondary"
|
||||
>
|
||||
{__("Back to login")}
|
||||
|
||||
@@ -1,81 +1,261 @@
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Field, useToast } from "@probo/ui";
|
||||
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
|
||||
import type { FormEventHandler } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useState } from "react";
|
||||
import { Link, useSearchParams } from "react-router";
|
||||
import { buildEndpoint } from "/providers/RelayProviders";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
|
||||
const authMethod = searchParams.get("method");
|
||||
const initialMode = authMethod === "password" ? "password" : authMethod === "sso" ? "sso" : "default";
|
||||
|
||||
const [mode, setMode] = useState<"default" | "password" | "sso">(initialMode);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
|
||||
const handlePasswordLogin: FormEventHandler<HTMLFormElement> = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const email = formData.get("email")?.toString();
|
||||
const password = formData.get("password")?.toString();
|
||||
const emailValue = formData.get("email")?.toString();
|
||||
const passwordValue = formData.get("password")?.toString();
|
||||
|
||||
fetch(buildEndpoint("/api/console/v1/auth/login"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.message || __("Failed to login"));
|
||||
}
|
||||
window.location.href = "/";
|
||||
})
|
||||
.catch((e) => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: e.message as string,
|
||||
variant: "error",
|
||||
});
|
||||
if (!emailValue || !passwordValue) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(buildEndpoint("/auth/login"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email: emailValue, password: passwordValue }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.message || __("Failed to login"));
|
||||
}
|
||||
|
||||
window.location.href = "/";
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: e.message as string,
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSSOLogin: FormEventHandler<HTMLFormElement> = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const emailValue = formData.get("email")?.toString();
|
||||
|
||||
if (!emailValue) return;
|
||||
|
||||
setIsChecking(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(buildEndpoint("/auth/check-sso"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email: emailValue }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.message || __("SSO not available for this email domain"));
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ssoAvailable && data.samlConfigId) {
|
||||
window.location.href = buildEndpoint(
|
||||
`/auth/saml/login/${data.samlConfigId}`
|
||||
);
|
||||
} else {
|
||||
throw new Error(__("SSO not available for this email domain"));
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: e.message as string,
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setMode("default");
|
||||
};
|
||||
|
||||
if (mode === "default") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-center text-2xl font-bold">
|
||||
{__("Login to your account")}
|
||||
</h1>
|
||||
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
||||
{__("Choose your login method")}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => setMode("password")}
|
||||
>
|
||||
{__("Login with Email")}
|
||||
</Button>
|
||||
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-border"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span
|
||||
className="px-4 text-xs uppercase text-txt-secondary"
|
||||
style={{ backgroundColor: "var(--color-level-0)" }}
|
||||
>
|
||||
{__("Or")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => setMode("sso")}
|
||||
>
|
||||
{__("Login with SSO")}
|
||||
</Button>
|
||||
|
||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||
{__("Don't have an account ?")}{" "}
|
||||
<Link to="/authentication/register" className="underline hover:text-txt-primary">
|
||||
{__("Register")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-txt-secondary">
|
||||
{__("Forgot password?")}{" "}
|
||||
<Link
|
||||
to="/authentication/forgot-password"
|
||||
className="underline hover:text-txt-primary"
|
||||
>
|
||||
{__("Reset password")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === "password") {
|
||||
return (
|
||||
<form className="space-y-4" onSubmit={handlePasswordLogin}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
||||
>
|
||||
<IconChevronLeft size={20} />
|
||||
<span className="text-sm">{__("Back")}</span>
|
||||
</button>
|
||||
|
||||
<h1 className="text-center text-2xl font-bold">
|
||||
{__("Login with Email")}
|
||||
</h1>
|
||||
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
||||
{__("Enter your email and password")}
|
||||
</p>
|
||||
|
||||
<Field
|
||||
required
|
||||
placeholder={__("Email")}
|
||||
name="email"
|
||||
type="email"
|
||||
label={__("Email")}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Field
|
||||
required
|
||||
placeholder={__("Password")}
|
||||
name="password"
|
||||
type="password"
|
||||
label={__("Password")}
|
||||
/>
|
||||
|
||||
<Button className="w-full" disabled={isLoading}>
|
||||
{isLoading ? __("Logging in...") : __("Login")}
|
||||
</Button>
|
||||
|
||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||
{__("Don't have an account ?")}{" "}
|
||||
<Link to="/authentication/register" className="underline hover:text-txt-primary">
|
||||
{__("Register")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-txt-secondary">
|
||||
{__("Forgot password?")}{" "}
|
||||
<Link
|
||||
to="/authentication/forgot-password"
|
||||
className="underline hover:text-txt-primary"
|
||||
>
|
||||
{__("Reset password")}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<form className="space-y-4" onSubmit={handleSSOLogin}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
||||
>
|
||||
<IconChevronLeft size={20} />
|
||||
<span className="text-sm">{__("Back")}</span>
|
||||
</button>
|
||||
|
||||
<h1 className="text-center text-2xl font-bold">
|
||||
{__("Login to your account")}
|
||||
{__("Login with SSO")}
|
||||
</h1>
|
||||
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
||||
{__("Enter your email below to login to your account")}
|
||||
{__("Enter your work email to continue with SSO")}
|
||||
</p>
|
||||
|
||||
<Field
|
||||
required
|
||||
placeholder={__("Email")}
|
||||
placeholder={__("Work Email")}
|
||||
name="email"
|
||||
type="email"
|
||||
label={__("Email")}
|
||||
label={__("Work Email")}
|
||||
autoFocus
|
||||
/>
|
||||
<Field
|
||||
required
|
||||
placeholder={__("Password")}
|
||||
name="password"
|
||||
type="password"
|
||||
label={__("Password")}
|
||||
/>
|
||||
<Button className="w-full">{__("Login")}</Button>
|
||||
|
||||
<Button className="w-full" disabled={isChecking}>
|
||||
{isChecking ? __("Checking...") : __("Continue with SSO")}
|
||||
</Button>
|
||||
|
||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||
{__("Don't have an account ?")}{" "}
|
||||
<Link to="/auth/register" className="underline hover:text-txt-primary">
|
||||
<Link to="/authentication/register" className="underline hover:text-txt-primary">
|
||||
{__("Register")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||
{__("Forgot password?")}{" "}
|
||||
<Link
|
||||
to="/auth/forgot-password"
|
||||
className="underline hover:text-txt-primary"
|
||||
>
|
||||
{__("Reset password")}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function RegisterPage() {
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
const response = await fetch(
|
||||
buildEndpoint("/api/console/v1/auth/register"),
|
||||
buildEndpoint("/auth/register"),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -106,7 +106,7 @@ export default function RegisterPage() {
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{__("Already have an account?")}{" "}
|
||||
<Link
|
||||
to="/auth/login"
|
||||
to="/authentication/login"
|
||||
className="underline text-txt-primary hover:text-txt-secondary"
|
||||
>
|
||||
{__("Log in here")}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function ResetPasswordPage() {
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
buildEndpoint("/api/console/v1/auth/reset-password"),
|
||||
buildEndpoint("/auth/reset-password"),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -74,7 +74,7 @@ export default function ResetPasswordPage() {
|
||||
description: __("Password reset successfully"),
|
||||
variant: "success",
|
||||
});
|
||||
navigate("/auth/login", { replace: true });
|
||||
navigate("/authentication/login", { replace: true });
|
||||
});
|
||||
|
||||
usePageTitle(__("Reset password"));
|
||||
@@ -118,7 +118,7 @@ export default function ResetPasswordPage() {
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{__("Remember your password?")}{" "}
|
||||
<Link
|
||||
to="/auth/login"
|
||||
to="/authentication/login"
|
||||
className="underline text-txt-primary hover:text-txt-secondary"
|
||||
>
|
||||
{__("Log in here")}
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function SignupFromInvitationPage() {
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
buildEndpoint("/api/console/v1/auth/signup-from-invitation"),
|
||||
buildEndpoint("/auth/signup-from-invitation"),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -125,7 +125,7 @@ export default function SignupFromInvitationPage() {
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{__("Already have an account?")}{" "}
|
||||
<Link
|
||||
to="/auth/login"
|
||||
to="/authentication/login"
|
||||
className="underline text-txt-primary hover:text-txt-secondary"
|
||||
>
|
||||
{__("Log in here")}
|
||||
|
||||
@@ -1,213 +1,41 @@
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Field,
|
||||
FileButton,
|
||||
IconTrashCan,
|
||||
Label,
|
||||
PageHeader,
|
||||
Spinner,
|
||||
TabBadge,
|
||||
TabItem,
|
||||
Tabs,
|
||||
Tbody,
|
||||
Td,
|
||||
Textarea,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useConfirm,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { Outlet } from "react-router";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import type { PreloadedQuery } from "react-relay";
|
||||
import type { OrganizationGraph_ViewQuery } from "/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql";
|
||||
import { useFragment, usePreloadedQuery, usePaginationFragment } from "react-relay";
|
||||
import { usePreloadedQuery } from "react-relay";
|
||||
import { organizationViewQuery } from "/hooks/graph/OrganizationGraph";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { SortableTable, SortableTh } from "/components/SortableTable";
|
||||
import clsx from "clsx";
|
||||
import type { SettingsPageFragment$key } from "./__generated__/SettingsPageFragment.graphql";
|
||||
import type {
|
||||
SettingsPageMembershipsFragment$data,
|
||||
SettingsPageMembershipsFragment$key
|
||||
} from "./__generated__/SettingsPageMembershipsFragment.graphql";
|
||||
import type {
|
||||
SettingsPageInvitationsFragment$data,
|
||||
SettingsPageInvitationsFragment$key
|
||||
} from "./__generated__/SettingsPageInvitationsFragment.graphql";
|
||||
import { useState, type ChangeEventHandler, useEffect, useRef } from "react";
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { z } from "zod";
|
||||
import type { NodeOf } from "/types";
|
||||
import {
|
||||
IconSettingsGear2,
|
||||
IconPeopleAdd,
|
||||
IconStore,
|
||||
IconLock,
|
||||
PageHeader,
|
||||
TabLink,
|
||||
Tabs,
|
||||
} from "@probo/ui";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { InviteUserDialog } from "/components/organizations/InviteUserDialog";
|
||||
import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
|
||||
import { useNavigate } from "react-router";
|
||||
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
|
||||
import { CustomDomainManager } from "/components/customDomains/CustomDomainManager";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
|
||||
const organizationSchema = z.object({
|
||||
name: z.string().min(1, "Organization name is required"),
|
||||
description: z.string().optional(),
|
||||
websiteUrl: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
headquarterAddress: z.string().optional(),
|
||||
});
|
||||
|
||||
type OrganizationFormData = z.infer<typeof organizationSchema>;
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<OrganizationGraph_ViewQuery>;
|
||||
};
|
||||
import { graphql } from "relay-runtime";
|
||||
import type { SettingsPageFragment$key } from "./__generated__/SettingsPageFragment.graphql";
|
||||
import { useFragment } from "react-relay";
|
||||
|
||||
const organizationFragment = graphql`
|
||||
fragment SettingsPageFragment on Organization {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
horizontalLogoUrl
|
||||
description
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
customDomain {
|
||||
id
|
||||
domain
|
||||
sslStatus
|
||||
dnsRecords {
|
||||
type
|
||||
name
|
||||
value
|
||||
ttl
|
||||
purpose
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
sslExpiresAt
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
...GeneralSettingsTabFragment
|
||||
...MembersSettingsTabMembershipsFragment
|
||||
...MembersSettingsTabInvitationsFragment
|
||||
...DomainSettingsTabFragment
|
||||
...SAMLSettingsTabFragment
|
||||
}
|
||||
`;
|
||||
|
||||
const paginatedMembershipsFragment = graphql`
|
||||
fragment SettingsPageMembershipsFragment on Organization
|
||||
@refetchable(queryName: "SettingsMembershipsRefetchQuery")
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 20 }
|
||||
order: { type: "MembershipOrder", defaultValue: { direction: ASC, field: CREATED_AT } }
|
||||
after: { type: "CursorKey", defaultValue: null }
|
||||
before: { type: "CursorKey", defaultValue: null }
|
||||
last: { type: "Int", defaultValue: null }
|
||||
) {
|
||||
memberships(
|
||||
first: $first
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: $order
|
||||
) @connection(key: "SettingsPageMemberships_memberships") {
|
||||
__id
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fullName
|
||||
emailAddress
|
||||
role
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const paginatedInvitationsFragment = graphql`
|
||||
fragment SettingsPageInvitationsFragment on Organization
|
||||
@refetchable(queryName: "SettingsInvitationsRefetchQuery")
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 20 }
|
||||
order: { type: "InvitationOrder", defaultValue: { direction: ASC, field: CREATED_AT } }
|
||||
after: { type: "CursorKey", defaultValue: null }
|
||||
before: { type: "CursorKey", defaultValue: null }
|
||||
last: { type: "Int", defaultValue: null }
|
||||
) {
|
||||
invitations(
|
||||
first: $first
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: $order
|
||||
filter: {statuses: [PENDING, EXPIRED]}
|
||||
) @connection(key: "SettingsPageInvitations_invitations") {
|
||||
__id
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
email
|
||||
fullName
|
||||
role
|
||||
status
|
||||
expiresAt
|
||||
acceptedAt
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteInvitationMutation = graphql`
|
||||
mutation SettingsPage_DeleteInvitationMutation(
|
||||
$input: DeleteInvitationInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
deleteInvitation(input: $input) {
|
||||
deletedInvitationId @deleteEdge(connections: $connections)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const updateOrganizationMutation = graphql`
|
||||
mutation SettingsPage_UpdateMutation($input: UpdateOrganizationInput!) {
|
||||
updateOrganization(input: $input) {
|
||||
organization {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
horizontalLogoUrl
|
||||
description
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteHorizontalLogoMutation = graphql`
|
||||
mutation SettingsPage_DeleteHorizontalLogoMutation($input: DeleteOrganizationHorizontalLogoInput!) {
|
||||
deleteOrganizationHorizontalLogo(input: $input) {
|
||||
organization {
|
||||
id
|
||||
horizontalLogoUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<OrganizationGraph_ViewQuery>;
|
||||
};
|
||||
|
||||
export default function SettingsPage({ queryRef }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
const organizationId = useOrganizationId();
|
||||
const organizationKey = usePreloadedQuery(
|
||||
organizationViewQuery,
|
||||
@@ -218,683 +46,30 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
organizationKey
|
||||
);
|
||||
|
||||
const membershipsPagination = usePaginationFragment(
|
||||
paginatedMembershipsFragment,
|
||||
organizationKey as SettingsPageMembershipsFragment$key
|
||||
);
|
||||
|
||||
const invitationsPagination = usePaginationFragment(
|
||||
paginatedInvitationsFragment,
|
||||
organizationKey as SettingsPageInvitationsFragment$key
|
||||
);
|
||||
|
||||
const refetchMemberships = () => {
|
||||
membershipsPagination.refetch({}, { fetchPolicy: 'network-only' });
|
||||
};
|
||||
|
||||
const refetchInvitations = () => {
|
||||
invitationsPagination.refetch({}, { fetchPolicy: 'network-only' });
|
||||
};
|
||||
|
||||
const [updateOrganization, isUpdatingOrganization] = useMutationWithToasts(
|
||||
updateOrganizationMutation,
|
||||
{
|
||||
successMessage: __("Organization updated successfully"),
|
||||
errorMessage: __("Failed to update organization"),
|
||||
}
|
||||
);
|
||||
const [deleteHorizontalLogo, isDeletingHorizontalLogo] = useMutationWithToasts(
|
||||
deleteHorizontalLogoMutation,
|
||||
{
|
||||
successMessage: __("Horizontal logo deleted successfully"),
|
||||
errorMessage: __("Failed to delete horizontal logo"),
|
||||
}
|
||||
);
|
||||
const [deleteOrganization, isDeleting] = useDeleteOrganizationMutation();
|
||||
const memberships = membershipsPagination.data.memberships?.edges.map((edge) => edge.node) || [];
|
||||
const invitations = invitationsPagination.data.invitations?.edges.map((edge) => edge.node) || [];
|
||||
const [activeTab, setActiveTab] = useState<"memberships" | "invitations">("memberships");
|
||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||
const [horizontalLogoPreview, setHorizontalLogoPreview] = useState<string | null>(null);
|
||||
|
||||
const { formState, handleSubmit, register, reset } = useFormWithSchema(
|
||||
organizationSchema,
|
||||
{
|
||||
defaultValues: {
|
||||
name: organization.name || "",
|
||||
description: organization.description || "",
|
||||
websiteUrl: organization.websiteUrl || "",
|
||||
email: organization.email || "",
|
||||
headquarterAddress: organization.headquarterAddress || "",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const prevOrgDataRef = useRef({
|
||||
name: organization.name,
|
||||
description: organization.description,
|
||||
websiteUrl: organization.websiteUrl,
|
||||
email: organization.email,
|
||||
headquarterAddress: organization.headquarterAddress,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const prev = prevOrgDataRef.current;
|
||||
const hasFormFieldChanges =
|
||||
prev.name !== organization.name ||
|
||||
prev.description !== organization.description ||
|
||||
prev.websiteUrl !== organization.websiteUrl ||
|
||||
prev.email !== organization.email ||
|
||||
prev.headquarterAddress !== organization.headquarterAddress;
|
||||
|
||||
if (hasFormFieldChanges) {
|
||||
reset({
|
||||
name: organization.name || "",
|
||||
description: organization.description || "",
|
||||
websiteUrl: organization.websiteUrl || "",
|
||||
email: organization.email || "",
|
||||
headquarterAddress: organization.headquarterAddress || "",
|
||||
});
|
||||
setLogoPreview(null);
|
||||
setHorizontalLogoPreview(null);
|
||||
|
||||
prevOrgDataRef.current = {
|
||||
name: organization.name,
|
||||
description: organization.description,
|
||||
websiteUrl: organization.websiteUrl,
|
||||
email: organization.email,
|
||||
headquarterAddress: organization.headquarterAddress,
|
||||
};
|
||||
}
|
||||
}, [organization, reset]);
|
||||
|
||||
const onSubmit = handleSubmit((data: OrganizationFormData) => {
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
name: data.name,
|
||||
description: data.description || undefined,
|
||||
websiteUrl: data.websiteUrl || undefined,
|
||||
email: data.email || undefined,
|
||||
headquarterAddress: data.headquarterAddress || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const handleLogoChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setLogoPreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
logoFile: null,
|
||||
},
|
||||
},
|
||||
uploadables: {
|
||||
"input.logoFile": file,
|
||||
},
|
||||
onSuccess: () => {
|
||||
setLogoPreview(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleHorizontalLogoChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setHorizontalLogoPreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
horizontalLogoFile: null,
|
||||
},
|
||||
},
|
||||
uploadables: {
|
||||
"input.horizontalLogoFile": file,
|
||||
},
|
||||
onSuccess: () => {
|
||||
setHorizontalLogoPreview(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteDialogRef = useDialogRef();
|
||||
|
||||
const handleDeleteHorizontalLogo = () => {
|
||||
deleteHorizontalLogo({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
},
|
||||
},
|
||||
onSuccess: () => {
|
||||
deleteDialogRef.current?.close();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteOrganization = () => {
|
||||
return deleteOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
},
|
||||
connections: [],
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate("/", { replace: true });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title={__("Settings")} />
|
||||
|
||||
{/* Organization settings */}
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-medium">
|
||||
{__("Organization details")}
|
||||
</h2>
|
||||
{formState.isSubmitting && <Spinner />}
|
||||
</div>
|
||||
<Card padded className="space-y-4">
|
||||
<div>
|
||||
<Label>{__("Organization logo")}</Label>
|
||||
<div className="flex w-max items-center gap-4">
|
||||
<Avatar
|
||||
src={logoPreview || organization.logoUrl}
|
||||
name={organization.name}
|
||||
size="xl"
|
||||
/>
|
||||
<FileButton
|
||||
disabled={formState.isSubmitting || isUpdatingOrganization}
|
||||
onChange={handleLogoChange}
|
||||
variant="secondary"
|
||||
className="ml-auto"
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
>
|
||||
{isUpdatingOrganization ? __("Uploading...") : __("Change logo")}
|
||||
</FileButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>{__("Horizontal logo")}</Label>
|
||||
<p className="text-sm text-txt-tertiary mb-2">
|
||||
{__("Upload a horizontal version of your logo for use in documents")}
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
{(horizontalLogoPreview || organization.horizontalLogoUrl) && (
|
||||
<div className="border border-border-solid rounded-md p-4 bg-surface-secondary">
|
||||
<img
|
||||
src={horizontalLogoPreview || organization.horizontalLogoUrl || undefined}
|
||||
alt={__("Horizontal logo")}
|
||||
className="h-12 max-w-xs object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<FileButton
|
||||
disabled={formState.isSubmitting || isUpdatingOrganization}
|
||||
onChange={handleHorizontalLogoChange}
|
||||
variant="secondary"
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
>
|
||||
{isUpdatingOrganization
|
||||
? __("Uploading...")
|
||||
: (horizontalLogoPreview || organization.horizontalLogoUrl)
|
||||
? __("Change horizontal logo")
|
||||
: __("Upload horizontal logo")}
|
||||
</FileButton>
|
||||
{organization.horizontalLogoUrl && (
|
||||
<Dialog
|
||||
ref={deleteDialogRef}
|
||||
trigger={
|
||||
<Button
|
||||
type="button"
|
||||
variant="quaternary"
|
||||
icon={IconTrashCan}
|
||||
aria-label={__("Delete horizontal logo")}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
/>
|
||||
}
|
||||
title={__("Delete Horizontal Logo")}
|
||||
className="max-w-md"
|
||||
>
|
||||
<DialogContent padded>
|
||||
<p className="text-txt-secondary">
|
||||
{__("Are you sure you want to delete the horizontal logo?")}
|
||||
</p>
|
||||
<p className="text-txt-secondary mt-2">
|
||||
{__("This action cannot be undone.")}
|
||||
</p>
|
||||
</DialogContent>
|
||||
<Tabs>
|
||||
<TabLink to={`/organizations/${organizationId}/settings/general`}>
|
||||
<IconSettingsGear2 size={20} />
|
||||
{__("General")}
|
||||
</TabLink>
|
||||
<TabLink to={`/organizations/${organizationId}/settings/members`}>
|
||||
<IconPeopleAdd size={20} />
|
||||
{__("Members")}
|
||||
</TabLink>
|
||||
<TabLink to={`/organizations/${organizationId}/settings/domain`}>
|
||||
<IconStore size={20} />
|
||||
{__("Domain")}
|
||||
</TabLink>
|
||||
<TabLink to={`/organizations/${organizationId}/settings/saml-sso`}>
|
||||
<IconLock size={20} />
|
||||
{__("SAML SSO")}
|
||||
</TabLink>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleDeleteHorizontalLogo}
|
||||
disabled={isDeletingHorizontalLogo}
|
||||
icon={isDeletingHorizontalLogo ? Spinner : IconTrashCan}
|
||||
>
|
||||
{isDeletingHorizontalLogo ? __("Deleting...") : __("Delete")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Field
|
||||
{...register("name")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="name"
|
||||
type="text"
|
||||
label={__("Organization name")}
|
||||
placeholder={__("Organization name")}
|
||||
/>
|
||||
<div>
|
||||
<Label>{__("Description")}</Label>
|
||||
<Textarea
|
||||
{...register("description")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="description"
|
||||
placeholder={__("Brief description of your organization")}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Field
|
||||
{...register("websiteUrl")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="websiteUrl"
|
||||
type="url"
|
||||
label={__("Website URL")}
|
||||
placeholder={__("https://example.com")}
|
||||
/>
|
||||
<Field
|
||||
{...register("email")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="email"
|
||||
type="email"
|
||||
label={__("Email")}
|
||||
placeholder={__("contact@example.com")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>{__("Headquarter Address")}</Label>
|
||||
<Textarea
|
||||
{...register("headquarterAddress")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="headquarterAddress"
|
||||
placeholder={__("123 Main St, City, Country")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formState.isDirty && (
|
||||
<div className="flex justify-end pt-6">
|
||||
<Button type="submit" disabled={formState.isSubmitting || isUpdatingOrganization}>
|
||||
{(formState.isSubmitting || isUpdatingOrganization)
|
||||
? __("Updating...")
|
||||
: __("Update Organization")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</form>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-medium">{__("Workspace members")}</h2>
|
||||
<InviteUserDialog
|
||||
connectionId={invitationsPagination.data.invitations?.__id}
|
||||
onRefetch={refetchInvitations}
|
||||
>
|
||||
<Button variant="secondary">{__("Invite member")}</Button>
|
||||
</InviteUserDialog>
|
||||
</div>
|
||||
|
||||
<Tabs>
|
||||
<TabItem
|
||||
active={activeTab === "memberships"}
|
||||
onClick={() => setActiveTab("memberships")}
|
||||
>
|
||||
{__("Members")}
|
||||
{(membershipsPagination.data.memberships?.totalCount || 0) > 0 && (
|
||||
<TabBadge>{membershipsPagination.data.memberships?.totalCount}</TabBadge>
|
||||
)}
|
||||
</TabItem>
|
||||
<TabItem
|
||||
active={activeTab === "invitations"}
|
||||
onClick={() => setActiveTab("invitations")}
|
||||
>
|
||||
{__("Invitations")}
|
||||
{(invitationsPagination.data.invitations?.totalCount || 0) > 0 && (
|
||||
<TabBadge>{invitationsPagination.data.invitations?.totalCount}</TabBadge>
|
||||
)}
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Card>
|
||||
<div className="px-6 pb-6 pt-6">
|
||||
{activeTab === "memberships" && (
|
||||
<SortableTable
|
||||
{...membershipsPagination}
|
||||
refetch={({ order }: { order: { direction: string; field: string } }) => {
|
||||
membershipsPagination.refetch({
|
||||
order: {
|
||||
direction: order.direction as "ASC" | "DESC",
|
||||
field: order.field as "CREATED_AT" | "FULL_NAME" | "EMAIL_ADDRESS" | "ROLE"
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh>
|
||||
<SortableTh field="EMAIL_ADDRESS">{__("Email")}</SortableTh>
|
||||
<SortableTh field="ROLE">{__("Role")}</SortableTh>
|
||||
<SortableTh field="CREATED_AT">{__("Joined")}</SortableTh>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{memberships.length === 0 ? (
|
||||
<Tr>
|
||||
<Td colSpan={5} className="text-center text-txt-secondary">
|
||||
{__("No members")}
|
||||
</Td>
|
||||
</Tr>
|
||||
) : (
|
||||
memberships.map((membership) => (
|
||||
<MembershipRow
|
||||
key={membership.id}
|
||||
membership={membership}
|
||||
connectionId={membershipsPagination.data.memberships?.__id}
|
||||
organizationId={organizationId}
|
||||
onRefetch={refetchMemberships}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
)}
|
||||
|
||||
{activeTab === "invitations" && (
|
||||
<SortableTable
|
||||
{...invitationsPagination}
|
||||
refetch={({ order }: { order: { direction: string; field: string } }) => {
|
||||
invitationsPagination.refetch({
|
||||
order: {
|
||||
direction: order.direction as "ASC" | "DESC",
|
||||
field: order.field as "CREATED_AT" | "EXPIRES_AT" | "FULL_NAME" | "EMAIL" | "ROLE" | "STATUS" | "ACCEPTED_AT"
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh>
|
||||
<SortableTh field="EMAIL">{__("Email")}</SortableTh>
|
||||
<SortableTh field="ROLE">{__("Role")}</SortableTh>
|
||||
<SortableTh field="CREATED_AT">{__("Invited")}</SortableTh>
|
||||
<Th>{__("Status")}</Th>
|
||||
<SortableTh field="ACCEPTED_AT">{__("Accepted at")}</SortableTh>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{invitations.length === 0 ? (
|
||||
<Tr>
|
||||
<Td colSpan={7} className="text-center text-txt-secondary">
|
||||
{__("No invitations")}
|
||||
</Td>
|
||||
</Tr>
|
||||
) : (
|
||||
invitations.map((invitation) => (
|
||||
<InvitationRow
|
||||
key={invitation.id}
|
||||
invitation={invitation}
|
||||
connectionId={invitationsPagination.data.invitations?.__id}
|
||||
organizationId={organizationId}
|
||||
onRefetch={refetchInvitations}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-base font-medium">{__("Custom Domain")}</h2>
|
||||
<CustomDomainManager
|
||||
organizationId={organization.id}
|
||||
customDomain={organization.customDomain}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-base font-medium text-red-600">
|
||||
{__("Danger Zone")}
|
||||
</h2>
|
||||
<Card padded className="border-red-200 flex items-center gap-3">
|
||||
<div className="mr-auto">
|
||||
<h3 className="text-base font-semibold text-red-700">
|
||||
{__("Delete Organization")}
|
||||
</h3>
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{__("Permanently delete this organization and all its data.")}{" "}
|
||||
<span className="text-red-600 font-medium">
|
||||
{__("This action cannot be undone.")}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<DeleteOrganizationDialog
|
||||
organizationName={organization.name}
|
||||
onConfirm={handleDeleteOrganization}
|
||||
isDeleting={isDeleting}
|
||||
>
|
||||
<Button variant="danger" icon={IconTrashCan} disabled={isDeleting}>
|
||||
{isDeleting ? __("Deleting...") : __("Delete Organization")}
|
||||
</Button>
|
||||
</DeleteOrganizationDialog>
|
||||
</Card>
|
||||
</div>
|
||||
<Outlet context={{ organization }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const removeMemberMutation = graphql`
|
||||
mutation SettingsPage_RemoveMemberMutation(
|
||||
$input: RemoveMemberInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
removeMember(input: $input) {
|
||||
deletedMemberId @deleteEdge(connections: $connections)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function InvitationRow(props: {
|
||||
invitation: NodeOf<SettingsPageInvitationsFragment$data["invitations"]>;
|
||||
connectionId?: string;
|
||||
organizationId: string;
|
||||
onRefetch: () => void;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const confirm = useConfirm();
|
||||
const [deleteInvitation, isDeleting] = useMutationWithToasts(
|
||||
deleteInvitationMutation,
|
||||
{
|
||||
successMessage: __("Invitation deleted successfully"),
|
||||
errorMessage: __("Failed to delete invitation"),
|
||||
}
|
||||
);
|
||||
|
||||
const onDelete = () => {
|
||||
confirm(
|
||||
() => {
|
||||
return deleteInvitation({
|
||||
variables: {
|
||||
input: {
|
||||
invitationId: props.invitation.id,
|
||||
},
|
||||
connections: props.connectionId ? [props.connectionId] : [],
|
||||
},
|
||||
onCompleted: () => {
|
||||
props.onRefetch();
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
message: sprintf(
|
||||
__("Are you sure you want to delete the invitation for %s?"),
|
||||
props.invitation.fullName
|
||||
),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tr className={clsx(isDeleting && "opacity-60 pointer-events-none")}>
|
||||
<Td>
|
||||
<div className="font-semibold">{props.invitation.fullName}</div>
|
||||
</Td>
|
||||
<Td>{props.invitation.email}</Td>
|
||||
<Td>
|
||||
<Badge>{props.invitation.role}</Badge>
|
||||
</Td>
|
||||
<Td>{new Date(props.invitation.createdAt).toLocaleDateString()}</Td>
|
||||
<Td>
|
||||
{props.invitation.status === "ACCEPTED" ? (
|
||||
<Badge variant="success">{__("Accepted")}</Badge>
|
||||
) : props.invitation.status === "EXPIRED" ? (
|
||||
<Badge variant="danger">{__("Expired")}</Badge>
|
||||
) : (
|
||||
<Badge variant="warning">{__("Pending")}</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{props.invitation.acceptedAt ? new Date(props.invitation.acceptedAt).toLocaleDateString() : "-"}
|
||||
</Td>
|
||||
<Td noLink width={80} className="text-end">
|
||||
<div
|
||||
className="flex gap-2 justify-end"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Spinner size={16} />
|
||||
) : (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={onDelete}
|
||||
disabled={isDeleting}
|
||||
icon={IconTrashCan}
|
||||
aria-label={__("Delete invitation")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
|
||||
function MembershipRow(props: {
|
||||
membership: NodeOf<SettingsPageMembershipsFragment$data["memberships"]>;
|
||||
connectionId?: string;
|
||||
organizationId: string;
|
||||
onRefetch: () => void;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
|
||||
successMessage: __("Member removed successfully"),
|
||||
errorMessage: __("Failed to remove member"),
|
||||
});
|
||||
const confirm = useConfirm();
|
||||
const [isRemoved, setIsRemoved] = useState(false);
|
||||
|
||||
if (isRemoved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onRemove = async () => {
|
||||
confirm(
|
||||
() => {
|
||||
return removeMember({
|
||||
variables: {
|
||||
input: {
|
||||
memberId: props.membership.id,
|
||||
organizationId: props.organizationId,
|
||||
},
|
||||
connections: props.connectionId ? [props.connectionId] : [],
|
||||
},
|
||||
onCompleted: () => {
|
||||
setIsRemoved(true);
|
||||
props.onRefetch();
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
message: sprintf(
|
||||
__("Are you sure you want to remove %s?"),
|
||||
props.membership.fullName
|
||||
),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tr className={clsx(isRemoving && "opacity-60 pointer-events-none")}>
|
||||
<Td>
|
||||
<div className="font-semibold">{props.membership.fullName}</div>
|
||||
</Td>
|
||||
<Td>{props.membership.emailAddress}</Td>
|
||||
<Td>
|
||||
<Badge>{props.membership.role}</Badge>
|
||||
</Td>
|
||||
<Td>{new Date(props.membership.createdAt).toLocaleDateString()}</Td>
|
||||
<Td noLink width={80} className="text-end">
|
||||
<div
|
||||
className="flex gap-2 justify-end"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isRemoving ? (
|
||||
<Spinner size={16} />
|
||||
) : (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={onRemove}
|
||||
disabled={isRemoving}
|
||||
icon={IconTrashCan}
|
||||
aria-label={__("Remove member")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<cf0ce3993d747999dfbe2ca60b509023>>
|
||||
* @generated SignedSource<<552529ec3c732ce161f2dcb4a16179b9>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -9,34 +9,11 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
export type SSLStatus = "ACTIVE" | "EXPIRED" | "FAILED" | "PENDING" | "PROVISIONING" | "RENEWING";
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type SettingsPageFragment$data = {
|
||||
readonly createdAt: any;
|
||||
readonly customDomain: {
|
||||
readonly createdAt: any;
|
||||
readonly dnsRecords: ReadonlyArray<{
|
||||
readonly name: string;
|
||||
readonly purpose: string;
|
||||
readonly ttl: number;
|
||||
readonly type: string;
|
||||
readonly value: string;
|
||||
}>;
|
||||
readonly domain: string;
|
||||
readonly id: string;
|
||||
readonly sslExpiresAt: any | null | undefined;
|
||||
readonly sslStatus: SSLStatus;
|
||||
readonly updatedAt: any;
|
||||
} | null | undefined;
|
||||
readonly description: string | null | undefined;
|
||||
readonly email: string | null | undefined;
|
||||
readonly headquarterAddress: string | null | undefined;
|
||||
readonly horizontalLogoUrl: string | null | undefined;
|
||||
readonly id: string;
|
||||
readonly logoUrl: string | null | undefined;
|
||||
readonly name: string;
|
||||
readonly updatedAt: any;
|
||||
readonly websiteUrl: string | null | undefined;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"DomainSettingsTabFragment" | "GeneralSettingsTabFragment" | "MembersSettingsTabInvitationsFragment" | "MembersSettingsTabMembershipsFragment" | "SAMLSettingsTabFragment">;
|
||||
readonly " $fragmentType": "SettingsPageFragment";
|
||||
};
|
||||
export type SettingsPageFragment$key = {
|
||||
@@ -44,168 +21,56 @@ export type SettingsPageFragment$key = {
|
||||
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment">;
|
||||
};
|
||||
|
||||
const node: ReaderFragment = (function(){
|
||||
var v0 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v1 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
const node: ReaderFragment = {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SettingsPageFragment",
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "horizontalLogoUrl",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
"kind": "FragmentSpread",
|
||||
"name": "GeneralSettingsTabFragment"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "websiteUrl",
|
||||
"storageKey": null
|
||||
"kind": "FragmentSpread",
|
||||
"name": "MembersSettingsTabMembershipsFragment"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
"kind": "FragmentSpread",
|
||||
"name": "MembersSettingsTabInvitationsFragment"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "headquarterAddress",
|
||||
"storageKey": null
|
||||
"kind": "FragmentSpread",
|
||||
"name": "DomainSettingsTabFragment"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "CustomDomain",
|
||||
"kind": "LinkedField",
|
||||
"name": "customDomain",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "domain",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "sslStatus",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "DNSRecordInstruction",
|
||||
"kind": "LinkedField",
|
||||
"name": "dnsRecords",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "type",
|
||||
"storageKey": null
|
||||
},
|
||||
(v1/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "value",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "ttl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "purpose",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "sslExpiresAt",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/)
|
||||
"kind": "FragmentSpread",
|
||||
"name": "SAMLSettingsTabFragment"
|
||||
}
|
||||
],
|
||||
"type": "Organization",
|
||||
"abstractKey": null
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "1e64bcccf8ef3a8ead79b3446e8a3ccd";
|
||||
(node as any).hash = "4f0ec089ac8ee79935eb56c22de31eca";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useOutletContext } from "react-router";
|
||||
import { useFragment, graphql } from "react-relay";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { CustomDomainManager } from "/components/customDomains/CustomDomainManager";
|
||||
import type { DomainSettingsTabFragment$key } from "./__generated__/DomainSettingsTabFragment.graphql";
|
||||
|
||||
const domainSettingsTabFragment = graphql`
|
||||
fragment DomainSettingsTabFragment on Organization {
|
||||
id
|
||||
customDomain {
|
||||
id
|
||||
domain
|
||||
sslStatus
|
||||
dnsRecords {
|
||||
type
|
||||
name
|
||||
value
|
||||
ttl
|
||||
purpose
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
sslExpiresAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type OutletContext = {
|
||||
organization: DomainSettingsTabFragment$key;
|
||||
};
|
||||
|
||||
export default function DomainSettingsTab() {
|
||||
const { __ } = useTranslate();
|
||||
const { organization: organizationKey } = useOutletContext<OutletContext>();
|
||||
const organization = useFragment(domainSettingsTabFragment, organizationKey);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-base font-medium">{__("Custom Domain")}</h2>
|
||||
<CustomDomainManager
|
||||
organizationId={organization.id}
|
||||
customDomain={organization.customDomain}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
import { useState, useRef, useEffect, type ChangeEventHandler } from "react";
|
||||
import { useOutletContext, useNavigate } from "react-router";
|
||||
import { useFragment, graphql } from "react-relay";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Field,
|
||||
FileButton,
|
||||
IconTrashCan,
|
||||
Label,
|
||||
Spinner,
|
||||
Textarea,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { z } from "zod";
|
||||
import type { GeneralSettingsTabFragment$key } from "./__generated__/GeneralSettingsTabFragment.graphql";
|
||||
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
|
||||
import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
|
||||
|
||||
const generalSettingsTabFragment = graphql`
|
||||
fragment GeneralSettingsTabFragment on Organization {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
horizontalLogoUrl
|
||||
description
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
`;
|
||||
|
||||
const updateOrganizationMutation = graphql`
|
||||
mutation GeneralSettingsTab_UpdateMutation($input: UpdateOrganizationInput!) {
|
||||
updateOrganization(input: $input) {
|
||||
organization {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
horizontalLogoUrl
|
||||
description
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteHorizontalLogoMutation = graphql`
|
||||
mutation GeneralSettingsTab_DeleteHorizontalLogoMutation(
|
||||
$input: DeleteOrganizationHorizontalLogoInput!
|
||||
) {
|
||||
deleteOrganizationHorizontalLogo(input: $input) {
|
||||
organization {
|
||||
id
|
||||
horizontalLogoUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const organizationSchema = z.object({
|
||||
name: z.string().min(1, "Organization name is required"),
|
||||
description: z.string().optional(),
|
||||
websiteUrl: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
headquarterAddress: z.string().optional(),
|
||||
});
|
||||
|
||||
type OrganizationFormData = z.infer<typeof organizationSchema>;
|
||||
|
||||
type OutletContext = {
|
||||
organization: GeneralSettingsTabFragment$key;
|
||||
};
|
||||
|
||||
export default function GeneralSettingsTab() {
|
||||
const { __ } = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
const { organization: organizationKey } = useOutletContext<OutletContext>();
|
||||
const organization = useFragment(generalSettingsTabFragment, organizationKey);
|
||||
const deleteDialogRef = useDialogRef();
|
||||
|
||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||
const [horizontalLogoPreview, setHorizontalLogoPreview] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const [updateOrganization, isUpdatingOrganization] = useMutationWithToasts(
|
||||
updateOrganizationMutation,
|
||||
{
|
||||
successMessage: __("Organization updated successfully"),
|
||||
errorMessage: __("Failed to update organization"),
|
||||
}
|
||||
);
|
||||
const [deleteHorizontalLogo, isDeletingHorizontalLogo] =
|
||||
useMutationWithToasts(deleteHorizontalLogoMutation, {
|
||||
successMessage: __("Horizontal logo deleted successfully"),
|
||||
errorMessage: __("Failed to delete horizontal logo"),
|
||||
});
|
||||
const [deleteOrganization, isDeletingOrganization] =
|
||||
useDeleteOrganizationMutation();
|
||||
|
||||
const { formState, handleSubmit, register, reset } = useFormWithSchema(
|
||||
organizationSchema,
|
||||
{
|
||||
defaultValues: {
|
||||
name: organization.name || "",
|
||||
description: organization.description || "",
|
||||
websiteUrl: organization.websiteUrl || "",
|
||||
email: organization.email || "",
|
||||
headquarterAddress: organization.headquarterAddress || "",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const prevOrgDataRef = useRef({
|
||||
name: organization.name,
|
||||
description: organization.description,
|
||||
websiteUrl: organization.websiteUrl,
|
||||
email: organization.email,
|
||||
headquarterAddress: organization.headquarterAddress,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const prevData = prevOrgDataRef.current;
|
||||
const currentData = {
|
||||
name: organization.name,
|
||||
description: organization.description,
|
||||
websiteUrl: organization.websiteUrl,
|
||||
email: organization.email,
|
||||
headquarterAddress: organization.headquarterAddress,
|
||||
};
|
||||
|
||||
if (JSON.stringify(prevData) !== JSON.stringify(currentData)) {
|
||||
reset({
|
||||
name: organization.name || "",
|
||||
description: organization.description || "",
|
||||
websiteUrl: organization.websiteUrl || "",
|
||||
email: organization.email || "",
|
||||
headquarterAddress: organization.headquarterAddress || "",
|
||||
});
|
||||
prevOrgDataRef.current = currentData;
|
||||
}
|
||||
}, [organization, reset]);
|
||||
|
||||
const onSubmit = handleSubmit((data: OrganizationFormData) => {
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
name: data.name,
|
||||
description: data.description || null,
|
||||
websiteUrl: data.websiteUrl || null,
|
||||
email: data.email || null,
|
||||
headquarterAddress: data.headquarterAddress || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const handleLogoChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setLogoPreview(reader.result as string);
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
logo: file,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
setLogoPreview(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleHorizontalLogoChange: ChangeEventHandler<HTMLInputElement> = (
|
||||
e
|
||||
) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setHorizontalLogoPreview(reader.result as string);
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
horizontalLogo: file,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
setHorizontalLogoPreview(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleDeleteHorizontalLogo = () => {
|
||||
deleteHorizontalLogo({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
deleteDialogRef.current?.close();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteOrganization = () => {
|
||||
return deleteOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
},
|
||||
connections: [],
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate("/", { replace: true });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-medium">
|
||||
{__("Organization details")}
|
||||
</h2>
|
||||
{formState.isSubmitting && <Spinner />}
|
||||
</div>
|
||||
<Card padded className="space-y-4">
|
||||
<div>
|
||||
<Label>{__("Organization logo")}</Label>
|
||||
<div className="flex w-max items-center gap-4">
|
||||
<Avatar
|
||||
src={logoPreview || organization.logoUrl}
|
||||
name={organization.name}
|
||||
size="xl"
|
||||
/>
|
||||
<FileButton
|
||||
disabled={formState.isSubmitting || isUpdatingOrganization}
|
||||
onChange={handleLogoChange}
|
||||
variant="secondary"
|
||||
className="ml-auto"
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
>
|
||||
{isUpdatingOrganization
|
||||
? __("Uploading...")
|
||||
: __("Change logo")}
|
||||
</FileButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>{__("Horizontal logo")}</Label>
|
||||
<p className="text-sm text-txt-tertiary mb-2">
|
||||
{__(
|
||||
"Upload a horizontal version of your logo for use in documents"
|
||||
)}
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
{(horizontalLogoPreview || organization.horizontalLogoUrl) && (
|
||||
<div className="border border-border-solid rounded-md p-4 bg-surface-secondary">
|
||||
<img
|
||||
src={
|
||||
horizontalLogoPreview ||
|
||||
organization.horizontalLogoUrl ||
|
||||
undefined
|
||||
}
|
||||
alt={__("Horizontal logo")}
|
||||
className="h-12 max-w-xs object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<FileButton
|
||||
disabled={formState.isSubmitting || isUpdatingOrganization}
|
||||
onChange={handleHorizontalLogoChange}
|
||||
variant="secondary"
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
>
|
||||
{isUpdatingOrganization
|
||||
? __("Uploading...")
|
||||
: horizontalLogoPreview || organization.horizontalLogoUrl
|
||||
? __("Change horizontal logo")
|
||||
: __("Upload horizontal logo")}
|
||||
</FileButton>
|
||||
{organization.horizontalLogoUrl && (
|
||||
<Dialog
|
||||
ref={deleteDialogRef}
|
||||
trigger={
|
||||
<Button
|
||||
type="button"
|
||||
variant="quaternary"
|
||||
icon={IconTrashCan}
|
||||
aria-label={__("Delete horizontal logo")}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
/>
|
||||
}
|
||||
title={__("Delete Horizontal Logo")}
|
||||
className="max-w-md"
|
||||
>
|
||||
<DialogContent padded>
|
||||
<p className="text-txt-secondary">
|
||||
{__(
|
||||
"Are you sure you want to delete the horizontal logo?"
|
||||
)}
|
||||
</p>
|
||||
<p className="text-txt-secondary mt-2">
|
||||
{__("This action cannot be undone.")}
|
||||
</p>
|
||||
</DialogContent>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleDeleteHorizontalLogo}
|
||||
disabled={isDeletingHorizontalLogo}
|
||||
icon={isDeletingHorizontalLogo ? Spinner : IconTrashCan}
|
||||
>
|
||||
{isDeletingHorizontalLogo
|
||||
? __("Deleting...")
|
||||
: __("Delete")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Field
|
||||
{...register("name")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="name"
|
||||
type="text"
|
||||
label={__("Organization name")}
|
||||
placeholder={__("Organization name")}
|
||||
/>
|
||||
<div>
|
||||
<Label>{__("Description")}</Label>
|
||||
<Textarea
|
||||
{...register("description")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="description"
|
||||
placeholder={__("Brief description of your organization")}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Field
|
||||
{...register("websiteUrl")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="websiteUrl"
|
||||
type="url"
|
||||
label={__("Website URL")}
|
||||
placeholder={__("https://example.com")}
|
||||
/>
|
||||
<Field
|
||||
{...register("email")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="email"
|
||||
type="email"
|
||||
label={__("Email")}
|
||||
placeholder={__("contact@example.com")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>{__("Headquarter Address")}</Label>
|
||||
<Textarea
|
||||
{...register("headquarterAddress")}
|
||||
readOnly={formState.isSubmitting}
|
||||
name="headquarterAddress"
|
||||
placeholder={__("123 Main St, City, Country")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formState.isDirty && (
|
||||
<div className="flex justify-end pt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formState.isSubmitting || isUpdatingOrganization}
|
||||
>
|
||||
{formState.isSubmitting || isUpdatingOrganization
|
||||
? __("Updating...")
|
||||
: __("Update Organization")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mt-12">
|
||||
<h2 className="text-base font-medium text-red-600">
|
||||
{__("Danger Zone")}
|
||||
</h2>
|
||||
<Card padded className="border-red-200 flex items-center gap-3">
|
||||
<div className="mr-auto">
|
||||
<h3 className="text-base font-semibold text-red-700">
|
||||
{__("Delete Organization")}
|
||||
</h3>
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{__("Permanently delete this organization and all its data.")}{" "}
|
||||
<span className="text-red-600 font-medium">
|
||||
{__("This action cannot be undone.")}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<DeleteOrganizationDialog
|
||||
organizationName={organization.name}
|
||||
onConfirm={handleDeleteOrganization}
|
||||
isDeleting={isDeletingOrganization}
|
||||
>
|
||||
<Button
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
disabled={isDeletingOrganization}
|
||||
>
|
||||
{__("Delete Organization")}
|
||||
</Button>
|
||||
</DeleteOrganizationDialog>
|
||||
</Card>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
import { useState } from "react";
|
||||
import { useOutletContext } from "react-router";
|
||||
import { usePaginationFragment, graphql } from "react-relay";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
IconTrashCan,
|
||||
Spinner,
|
||||
TabBadge,
|
||||
TabItem,
|
||||
Tabs,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useConfirm,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { SortableTable, SortableTh } from "/components/SortableTable";
|
||||
import { InviteUserDialog } from "/components/organizations/InviteUserDialog";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import clsx from "clsx";
|
||||
import type { NodeOf } from "/types";
|
||||
import type {
|
||||
MembersSettingsTabMembershipsFragment$data,
|
||||
MembersSettingsTabMembershipsFragment$key
|
||||
} from "./__generated__/MembersSettingsTabMembershipsFragment.graphql";
|
||||
import type {
|
||||
MembersSettingsTabInvitationsFragment$data,
|
||||
MembersSettingsTabInvitationsFragment$key
|
||||
} from "./__generated__/MembersSettingsTabInvitationsFragment.graphql";
|
||||
|
||||
const paginatedMembershipsFragment = graphql`
|
||||
fragment MembersSettingsTabMembershipsFragment on Organization
|
||||
@refetchable(queryName: "MembersSettingsTabMembershipsRefetchQuery")
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 20 }
|
||||
order: { type: "MembershipOrder", defaultValue: { direction: ASC, field: CREATED_AT } }
|
||||
after: { type: "CursorKey", defaultValue: null }
|
||||
before: { type: "CursorKey", defaultValue: null }
|
||||
last: { type: "Int", defaultValue: null }
|
||||
) {
|
||||
memberships(
|
||||
first: $first
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: $order
|
||||
) @connection(key: "MembersSettingsTabMemberships_memberships") {
|
||||
__id
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fullName
|
||||
emailAddress
|
||||
role
|
||||
authMethod
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const paginatedInvitationsFragment = graphql`
|
||||
fragment MembersSettingsTabInvitationsFragment on Organization
|
||||
@refetchable(queryName: "MembersSettingsTabInvitationsRefetchQuery")
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 20 }
|
||||
order: { type: "InvitationOrder", defaultValue: { direction: ASC, field: CREATED_AT } }
|
||||
after: { type: "CursorKey", defaultValue: null }
|
||||
before: { type: "CursorKey", defaultValue: null }
|
||||
last: { type: "Int", defaultValue: null }
|
||||
) {
|
||||
invitations(
|
||||
first: $first
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: $order
|
||||
) @connection(key: "MembersSettingsTabInvitations_invitations") {
|
||||
__id
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fullName
|
||||
email
|
||||
role
|
||||
status
|
||||
createdAt
|
||||
expiresAt
|
||||
acceptedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const removeMemberMutation = graphql`
|
||||
mutation MembersSettingsTab_RemoveMemberMutation(
|
||||
$input: RemoveMemberInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
removeMember(input: $input) {
|
||||
deletedMemberId @deleteEdge(connections: $connections)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteInvitationMutation = graphql`
|
||||
mutation MembersSettingsTab_DeleteInvitationMutation(
|
||||
$input: DeleteInvitationInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
deleteInvitation(input: $input) {
|
||||
deletedInvitationId @deleteEdge(connections: $connections)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type OutletContext = {
|
||||
organization: MembersSettingsTabMembershipsFragment$key & MembersSettingsTabInvitationsFragment$key & { id: string };
|
||||
};
|
||||
|
||||
export default function MembersSettingsTab() {
|
||||
const { __ } = useTranslate();
|
||||
const { organization: organizationKey } = useOutletContext<OutletContext>();
|
||||
|
||||
const membershipsPagination = usePaginationFragment(
|
||||
paginatedMembershipsFragment,
|
||||
organizationKey as MembersSettingsTabMembershipsFragment$key
|
||||
);
|
||||
|
||||
const invitationsPagination = usePaginationFragment(
|
||||
paginatedInvitationsFragment,
|
||||
organizationKey as MembersSettingsTabInvitationsFragment$key
|
||||
);
|
||||
|
||||
const refetchMemberships = () => {
|
||||
membershipsPagination.refetch({}, { fetchPolicy: 'network-only' });
|
||||
};
|
||||
|
||||
const refetchInvitations = () => {
|
||||
invitationsPagination.refetch({}, { fetchPolicy: 'network-only' });
|
||||
};
|
||||
|
||||
const memberships = membershipsPagination.data.memberships?.edges.map((edge) => edge.node) || [];
|
||||
const invitations = invitationsPagination.data.invitations?.edges.map((edge) => edge.node) || [];
|
||||
const [activeTab, setActiveTab] = useState<"memberships" | "invitations">("memberships");
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-medium">{__("Workspace members")}</h2>
|
||||
<InviteUserDialog
|
||||
connectionId={invitationsPagination.data.invitations?.__id}
|
||||
onRefetch={refetchInvitations}
|
||||
>
|
||||
<Button variant="secondary">{__("Invite member")}</Button>
|
||||
</InviteUserDialog>
|
||||
</div>
|
||||
|
||||
<Tabs>
|
||||
<TabItem
|
||||
active={activeTab === "memberships"}
|
||||
onClick={() => setActiveTab("memberships")}
|
||||
>
|
||||
{__("Members")}
|
||||
{(membershipsPagination.data.memberships?.totalCount || 0) > 0 && (
|
||||
<TabBadge>{membershipsPagination.data.memberships?.totalCount}</TabBadge>
|
||||
)}
|
||||
</TabItem>
|
||||
<TabItem
|
||||
active={activeTab === "invitations"}
|
||||
onClick={() => setActiveTab("invitations")}
|
||||
>
|
||||
{__("Invitations")}
|
||||
{(invitationsPagination.data.invitations?.totalCount || 0) > 0 && (
|
||||
<TabBadge>{invitationsPagination.data.invitations?.totalCount}</TabBadge>
|
||||
)}
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Card>
|
||||
<div className="px-6 pb-6 pt-6">
|
||||
{activeTab === "memberships" && (
|
||||
<SortableTable
|
||||
{...membershipsPagination}
|
||||
refetch={({ order }: { order: { direction: string; field: string } }) => {
|
||||
membershipsPagination.refetch({
|
||||
order: {
|
||||
direction: order.direction as "ASC" | "DESC",
|
||||
field: order.field as "CREATED_AT" | "FULL_NAME" | "EMAIL_ADDRESS" | "ROLE"
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh>
|
||||
<SortableTh field="EMAIL_ADDRESS">{__("Email")}</SortableTh>
|
||||
<SortableTh field="ROLE">{__("Role")}</SortableTh>
|
||||
<SortableTh field="CREATED_AT">{__("Joined")}</SortableTh>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{memberships.length === 0 ? (
|
||||
<Tr>
|
||||
<Td colSpan={5} className="text-center text-txt-secondary">
|
||||
{__("No members")}
|
||||
</Td>
|
||||
</Tr>
|
||||
) : (
|
||||
memberships.map((membership) => (
|
||||
<MembershipRow
|
||||
key={membership.id}
|
||||
membership={membership}
|
||||
connectionId={membershipsPagination.data.memberships?.__id}
|
||||
organizationId={(organizationKey as { id: string }).id}
|
||||
onRefetch={refetchMemberships}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
)}
|
||||
|
||||
{activeTab === "invitations" && (
|
||||
<SortableTable
|
||||
{...invitationsPagination}
|
||||
refetch={({ order }: { order: { direction: string; field: string } }) => {
|
||||
invitationsPagination.refetch({
|
||||
order: {
|
||||
direction: order.direction as "ASC" | "DESC",
|
||||
field: order.field as "CREATED_AT" | "EXPIRES_AT" | "FULL_NAME" | "EMAIL" | "ROLE" | "STATUS" | "ACCEPTED_AT"
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh>
|
||||
<SortableTh field="EMAIL">{__("Email")}</SortableTh>
|
||||
<SortableTh field="ROLE">{__("Role")}</SortableTh>
|
||||
<SortableTh field="CREATED_AT">{__("Invited")}</SortableTh>
|
||||
<Th>{__("Status")}</Th>
|
||||
<SortableTh field="ACCEPTED_AT">{__("Accepted at")}</SortableTh>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{invitations.length === 0 ? (
|
||||
<Tr>
|
||||
<Td colSpan={7} className="text-center text-txt-secondary">
|
||||
{__("No invitations")}
|
||||
</Td>
|
||||
</Tr>
|
||||
) : (
|
||||
invitations.map((invitation) => (
|
||||
<InvitationRow
|
||||
key={invitation.id}
|
||||
invitation={invitation}
|
||||
connectionId={invitationsPagination.data.invitations?.__id}
|
||||
organizationId={(organizationKey as { id: string }).id}
|
||||
onRefetch={refetchInvitations}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InvitationRow(props: {
|
||||
invitation: NodeOf<MembersSettingsTabInvitationsFragment$data["invitations"]>;
|
||||
connectionId?: string;
|
||||
organizationId: string;
|
||||
onRefetch: () => void;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const confirm = useConfirm();
|
||||
const [deleteInvitation, isDeleting] = useMutationWithToasts(
|
||||
deleteInvitationMutation,
|
||||
{
|
||||
successMessage: __("Invitation deleted successfully"),
|
||||
errorMessage: __("Failed to delete invitation"),
|
||||
}
|
||||
);
|
||||
|
||||
const onDelete = () => {
|
||||
confirm(
|
||||
() => {
|
||||
return deleteInvitation({
|
||||
variables: {
|
||||
input: {
|
||||
invitationId: props.invitation.id,
|
||||
},
|
||||
connections: props.connectionId ? [props.connectionId] : [],
|
||||
},
|
||||
onCompleted: () => {
|
||||
props.onRefetch();
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
message: sprintf(
|
||||
__("Are you sure you want to delete the invitation for %s?"),
|
||||
props.invitation.fullName
|
||||
),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tr className={clsx(isDeleting && "opacity-60 pointer-events-none")}>
|
||||
<Td>
|
||||
<div className="font-semibold">{props.invitation.fullName}</div>
|
||||
</Td>
|
||||
<Td>{props.invitation.email}</Td>
|
||||
<Td>
|
||||
<Badge>{props.invitation.role}</Badge>
|
||||
</Td>
|
||||
<Td>{new Date(props.invitation.createdAt).toLocaleDateString()}</Td>
|
||||
<Td>
|
||||
{props.invitation.status === "ACCEPTED" ? (
|
||||
<Badge variant="success">{__("Accepted")}</Badge>
|
||||
) : props.invitation.status === "EXPIRED" ? (
|
||||
<Badge variant="danger">{__("Expired")}</Badge>
|
||||
) : (
|
||||
<Badge variant="warning">{__("Pending")}</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{props.invitation.acceptedAt ? new Date(props.invitation.acceptedAt).toLocaleDateString() : "-"}
|
||||
</Td>
|
||||
<Td noLink width={80} className="text-end">
|
||||
<div
|
||||
className="flex gap-2 justify-end"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Spinner size={16} />
|
||||
) : (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={onDelete}
|
||||
disabled={isDeleting}
|
||||
icon={IconTrashCan}
|
||||
aria-label={__("Delete invitation")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
|
||||
function MembershipRow(props: {
|
||||
membership: NodeOf<MembersSettingsTabMembershipsFragment$data["memberships"]>;
|
||||
connectionId?: string;
|
||||
organizationId: string;
|
||||
onRefetch: () => void;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
|
||||
successMessage: __("Member removed successfully"),
|
||||
errorMessage: __("Failed to remove member"),
|
||||
});
|
||||
const confirm = useConfirm();
|
||||
const [isRemoved, setIsRemoved] = useState(false);
|
||||
|
||||
if (isRemoved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onRemove = async () => {
|
||||
confirm(
|
||||
() => {
|
||||
return removeMember({
|
||||
variables: {
|
||||
input: {
|
||||
memberId: props.membership.id,
|
||||
organizationId: props.organizationId,
|
||||
},
|
||||
connections: props.connectionId ? [props.connectionId] : [],
|
||||
},
|
||||
onCompleted: () => {
|
||||
setIsRemoved(true);
|
||||
props.onRefetch();
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
message: sprintf(
|
||||
__("Are you sure you want to remove %s?"),
|
||||
props.membership.fullName
|
||||
),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tr className={clsx(isRemoving && "opacity-60 pointer-events-none")}>
|
||||
<Td>
|
||||
<div className="font-semibold">{props.membership.fullName}</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
{props.membership.emailAddress}
|
||||
{props.membership.authMethod === "SAML" && (
|
||||
<Badge variant="info">SAML</Badge>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge>{props.membership.role}</Badge>
|
||||
</Td>
|
||||
<Td>{new Date(props.membership.createdAt).toLocaleDateString()}</Td>
|
||||
<Td noLink width={80} className="text-end">
|
||||
<div
|
||||
className="flex gap-2 justify-end"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isRemoving ? (
|
||||
<Spinner size={16} />
|
||||
) : (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={onRemove}
|
||||
disabled={isRemoving}
|
||||
icon={IconTrashCan}
|
||||
aria-label={__("Remove member")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,783 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useOutletContext } from "react-router";
|
||||
import { useFragment, graphql } from "react-relay";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
Field,
|
||||
Label,
|
||||
Option,
|
||||
Select,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Textarea,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useConfirm,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import {
|
||||
useCreateSAMLConfigurationMutation,
|
||||
useUpdateSAMLConfigurationMutation,
|
||||
useDeleteSAMLConfigurationMutation,
|
||||
useEnableSAMLMutation,
|
||||
useDisableSAMLMutation,
|
||||
useInitiateDomainVerificationMutation,
|
||||
useVerifyDomainMutation,
|
||||
} from "/hooks/graph/SAMLConfigurationGraph";
|
||||
import type { SAMLSettingsTabFragment$key } from "./__generated__/SAMLSettingsTabFragment.graphql";
|
||||
|
||||
const samlSettingsTabFragment = graphql`
|
||||
fragment SAMLSettingsTabFragment on Organization {
|
||||
id
|
||||
name
|
||||
samlConfigurations {
|
||||
id
|
||||
enabled
|
||||
emailDomain
|
||||
enforcementPolicy
|
||||
domainVerified
|
||||
domainVerificationToken
|
||||
domainVerifiedAt
|
||||
spEntityId
|
||||
spAcsUrl
|
||||
spMetadataUrl
|
||||
testLoginUrl
|
||||
idpEntityId
|
||||
idpSsoUrl
|
||||
idpCertificate
|
||||
idpMetadataUrl
|
||||
attributeEmail
|
||||
attributeFirstname
|
||||
attributeLastname
|
||||
attributeRole
|
||||
defaultRole
|
||||
autoSignupEnabled
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const initiateSchema = 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)"),
|
||||
});
|
||||
|
||||
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"),
|
||||
idpMetadataUrl: z.string().url("IdP Metadata URL must be a valid URL").optional().or(z.literal("")),
|
||||
attributeEmail: z.string().optional(),
|
||||
attributeFirstname: z.string().optional(),
|
||||
attributeLastname: z.string().optional(),
|
||||
attributeRole: z.string().optional(),
|
||||
defaultRole: z.string().optional(),
|
||||
autoSignupEnabled: z.boolean().default(false),
|
||||
});
|
||||
|
||||
type OutletContext = {
|
||||
organization: SAMLSettingsTabFragment$key;
|
||||
};
|
||||
|
||||
type SetupStep = "initiate" | "verify" | "configure";
|
||||
|
||||
export default function SAMLSettingsTab() {
|
||||
const { __ } = useTranslate();
|
||||
const { organization: organizationKey } = useOutletContext<OutletContext>();
|
||||
const organization = useFragment(samlSettingsTabFragment, organizationKey);
|
||||
const configs = organization.samlConfigurations;
|
||||
|
||||
const dialogRef = useDialogRef();
|
||||
const [editingConfig, setEditingConfig] = useState<typeof configs[0] | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState<SetupStep>("initiate");
|
||||
const [dnsRecord, setDnsRecord] = useState<string>("");
|
||||
|
||||
const [createMutation, isCreating] = useCreateSAMLConfigurationMutation();
|
||||
const [updateMutation, isUpdating] = useUpdateSAMLConfigurationMutation();
|
||||
const [deleteMutation] = useDeleteSAMLConfigurationMutation();
|
||||
const [enableMutation, isEnabling] = useEnableSAMLMutation();
|
||||
const [disableMutation, isDisabling] = useDisableSAMLMutation();
|
||||
const [initiateDomainMutation, isInitiating] = useInitiateDomainVerificationMutation();
|
||||
const [verifyDomainMutation, isVerifying] = useVerifyDomainMutation();
|
||||
|
||||
const confirm = useConfirm();
|
||||
|
||||
const handleOpenModal = (config?: typeof configs[0]) => {
|
||||
setEditingConfig(config || null);
|
||||
if (config) {
|
||||
if (!config.domainVerified) {
|
||||
setCurrentStep("verify");
|
||||
setDnsRecord(`probo-verification=${config.domainVerificationToken}`);
|
||||
} else {
|
||||
setCurrentStep("configure");
|
||||
}
|
||||
} else {
|
||||
setCurrentStep("initiate");
|
||||
}
|
||||
dialogRef.current?.open();
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setEditingConfig(null);
|
||||
setCurrentStep("initiate");
|
||||
setDnsRecord("");
|
||||
dialogRef.current?.close();
|
||||
};
|
||||
|
||||
const initiateForm = useFormWithSchema(initiateSchema, {
|
||||
defaultValues: {
|
||||
emailDomain: editingConfig?.emailDomain || "",
|
||||
},
|
||||
});
|
||||
|
||||
const form = useFormWithSchema(samlConfigSchema, {
|
||||
defaultValues: editingConfig
|
||||
? {
|
||||
emailDomain: editingConfig.emailDomain || "",
|
||||
enforcementPolicy: editingConfig.enforcementPolicy || "OPTIONAL",
|
||||
idpEntityId: editingConfig.idpEntityId || "",
|
||||
idpSsoUrl: editingConfig.idpSsoUrl || "",
|
||||
idpCertificate: editingConfig.idpCertificate || "",
|
||||
idpMetadataUrl: editingConfig.idpMetadataUrl || "",
|
||||
attributeEmail: editingConfig.attributeEmail || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
attributeFirstname: editingConfig.attributeFirstname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
|
||||
attributeLastname: editingConfig.attributeLastname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
|
||||
attributeRole: editingConfig.attributeRole || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
|
||||
defaultRole: editingConfig.defaultRole || "MEMBER",
|
||||
autoSignupEnabled: editingConfig.autoSignupEnabled || false,
|
||||
}
|
||||
: {
|
||||
emailDomain: "",
|
||||
enforcementPolicy: "OPTIONAL",
|
||||
idpEntityId: "",
|
||||
idpSsoUrl: "",
|
||||
idpCertificate: "",
|
||||
idpMetadataUrl: "",
|
||||
attributeEmail: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
attributeFirstname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
|
||||
attributeLastname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
|
||||
attributeRole: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
|
||||
defaultRole: "MEMBER",
|
||||
autoSignupEnabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editingConfig) {
|
||||
form.reset({
|
||||
emailDomain: editingConfig.emailDomain || "",
|
||||
enforcementPolicy: editingConfig.enforcementPolicy || "OPTIONAL",
|
||||
idpEntityId: editingConfig.idpEntityId || "",
|
||||
idpSsoUrl: editingConfig.idpSsoUrl || "",
|
||||
idpCertificate: editingConfig.idpCertificate || "",
|
||||
idpMetadataUrl: editingConfig.idpMetadataUrl || "",
|
||||
attributeEmail: editingConfig.attributeEmail || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
attributeFirstname: editingConfig.attributeFirstname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
|
||||
attributeLastname: editingConfig.attributeLastname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
|
||||
attributeRole: editingConfig.attributeRole || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
|
||||
defaultRole: editingConfig.defaultRole || "MEMBER",
|
||||
autoSignupEnabled: editingConfig.autoSignupEnabled || false,
|
||||
});
|
||||
initiateForm.reset({
|
||||
emailDomain: editingConfig.emailDomain || "",
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
emailDomain: "",
|
||||
enforcementPolicy: "OPTIONAL",
|
||||
idpEntityId: "",
|
||||
idpSsoUrl: "",
|
||||
idpCertificate: "",
|
||||
idpMetadataUrl: "",
|
||||
attributeEmail: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
attributeFirstname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
|
||||
attributeLastname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
|
||||
attributeRole: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
|
||||
defaultRole: "MEMBER",
|
||||
autoSignupEnabled: false,
|
||||
});
|
||||
initiateForm.reset({
|
||||
emailDomain: "",
|
||||
});
|
||||
}
|
||||
}, [editingConfig, form, initiateForm]);
|
||||
|
||||
const handleInitiateDomain = initiateForm.handleSubmit((data) => {
|
||||
initiateDomainMutation({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
emailDomain: data.emailDomain,
|
||||
},
|
||||
},
|
||||
onCompleted: (response) => {
|
||||
setDnsRecord(response.initiateDomainVerification.dnsRecord);
|
||||
setEditingConfig(response.initiateDomainVerification.samlConfiguration as any);
|
||||
setCurrentStep("verify");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const handleVerifyDomain = () => {
|
||||
if (!editingConfig) return;
|
||||
verifyDomainMutation({
|
||||
variables: {
|
||||
input: {
|
||||
id: editingConfig.id,
|
||||
},
|
||||
},
|
||||
onCompleted: (response) => {
|
||||
if (response.verifyDomain.verified) {
|
||||
setCurrentStep("configure");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
if (editingConfig) {
|
||||
updateMutation({
|
||||
variables: {
|
||||
input: {
|
||||
id: editingConfig.id,
|
||||
enforcementPolicy: data.enforcementPolicy,
|
||||
idpEntityId: data.idpEntityId,
|
||||
idpSsoUrl: data.idpSsoUrl,
|
||||
idpCertificate: data.idpCertificate,
|
||||
idpMetadataUrl: data.idpMetadataUrl || null,
|
||||
attributeEmail: data.attributeEmail || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
attributeFirstname: data.attributeFirstname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
|
||||
attributeLastname: data.attributeLastname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
|
||||
attributeRole: data.attributeRole || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
|
||||
defaultRole: data.defaultRole || "MEMBER",
|
||||
autoSignupEnabled: data.autoSignupEnabled || false,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
handleCloseModal();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
createMutation({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
emailDomain: data.emailDomain,
|
||||
enforcementPolicy: data.enforcementPolicy,
|
||||
idpEntityId: data.idpEntityId,
|
||||
idpSsoUrl: data.idpSsoUrl,
|
||||
idpCertificate: data.idpCertificate,
|
||||
idpMetadataUrl: data.idpMetadataUrl || null,
|
||||
attributeEmail: data.attributeEmail || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
attributeFirstname: data.attributeFirstname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
|
||||
attributeLastname: data.attributeLastname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
|
||||
attributeRole: data.attributeRole || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
|
||||
defaultRole: data.defaultRole || "MEMBER",
|
||||
autoSignupEnabled: data.autoSignupEnabled || false,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
handleCloseModal();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleToggleEnabled = (config: typeof configs[0]) => {
|
||||
if (config.enabled) {
|
||||
confirm(
|
||||
async () => {
|
||||
disableMutation({
|
||||
variables: {
|
||||
input: {
|
||||
id: config.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
title: __("Disable SAML"),
|
||||
message: __(
|
||||
"Are you sure you want to disable SAML authentication for " + config.emailDomain + "?"
|
||||
),
|
||||
label: __("Disable"),
|
||||
variant: "danger",
|
||||
}
|
||||
);
|
||||
} else {
|
||||
enableMutation({
|
||||
variables: {
|
||||
input: {
|
||||
id: config.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (config: typeof configs[0]) => {
|
||||
confirm(
|
||||
async () => {
|
||||
deleteMutation({
|
||||
variables: {
|
||||
input: {
|
||||
id: config.id,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
handleCloseModal();
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
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",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const [copiedUrl, setCopiedUrl] = useState<string | null>(null);
|
||||
|
||||
const handleCopy = (url: string) => {
|
||||
navigator.clipboard.writeText(url);
|
||||
setCopiedUrl(url);
|
||||
setTimeout(() => setCopiedUrl(null), 2000);
|
||||
};
|
||||
|
||||
const getEnforcementPolicyLabel = (policy: 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");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-base font-medium">{__("SAML Single Sign-On")}</h2>
|
||||
<Button onClick={() => handleOpenModal()}>
|
||||
{__("Add Configuration")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{configs.length === 0 ? (
|
||||
<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>
|
||||
<Button onClick={() => handleOpenModal()}>
|
||||
{__("Add Your First Configuration")}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<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>
|
||||
{configs.map((config) => (
|
||||
<Tr key={config.id}>
|
||||
<Td>
|
||||
<button
|
||||
onClick={() => handleOpenModal(config)}
|
||||
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.domainVerified
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-yellow-100 text-yellow-800"
|
||||
}`}
|
||||
>
|
||||
{config.domainVerified ? __("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.enabled
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-gray-100 text-gray-800"
|
||||
}`}
|
||||
>
|
||||
{config.enabled ? __("Enabled") : __("Disabled")}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>{config.enforcementPolicy}</Td>
|
||||
<Td>
|
||||
{config.domainVerified && config.enabled ? (
|
||||
<button
|
||||
onClick={() => handleCopy(config.testLoginUrl)}
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{copiedUrl === config.testLoginUrl ? __("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.domainVerified ? (
|
||||
<>
|
||||
<Button
|
||||
variant={config.enabled ? "danger" : "primary"}
|
||||
onClick={() => handleToggleEnabled(config)}
|
||||
disabled={isEnabling || isDisabling}
|
||||
>
|
||||
{config.enabled ? __("Disable") : __("Enable")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleOpenModal(config)}
|
||||
>
|
||||
{__("Edit")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleOpenModal(config)}
|
||||
>
|
||||
{__("Verify Domain")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => handleDelete(config)}
|
||||
>
|
||||
{__("Delete")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog ref={dialogRef} onClose={handleCloseModal}>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
{currentStep === "initiate" && __("Step 1: Register Domain")}
|
||||
{currentStep === "verify" && __("Step 2: Verify Domain Ownership")}
|
||||
{currentStep === "configure" && (editingConfig?.domainVerified ? __("Configure SAML") : __("Step 3: Configure SAML"))}
|
||||
</DialogTitle>
|
||||
|
||||
{currentStep === "initiate" && (
|
||||
<form onSubmit={handleInitiateDomain} className="space-y-6 p-6">
|
||||
<div>
|
||||
<h3 className="text-base font-medium mb-4">
|
||||
{__("Register Your Domain")}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
{__("To set up SAML SSO, you must first register and verify ownership of your email domain.")}
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<Field
|
||||
{...initiateForm.register("emailDomain")}
|
||||
label={__("Email Domain") + " *"}
|
||||
placeholder="example.com"
|
||||
error={initiateForm.formState.errors.emailDomain?.message}
|
||||
/>
|
||||
<p className="text-xs text-gray-600">
|
||||
{__("The email domain this SAML configuration will apply to (e.g., example.com)")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{currentStep === "verify" && (
|
||||
<div className="space-y-6 p-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={() => handleCopy(dnsRecord)}
|
||||
>
|
||||
{copiedUrl === dnsRecord ? __("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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === "configure" && (
|
||||
<form onSubmit={onSubmit} className="space-y-6 p-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={!!editingConfig}
|
||||
error={form.formState.errors.emailDomain?.message}
|
||||
/>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{editingConfig
|
||||
? __("Email domain cannot be changed after creation")
|
||||
: __("The email domain this SAML configuration applies to (e.g., example.com)")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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----- ... -----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>
|
||||
<Field
|
||||
{...form.register("idpMetadataUrl")}
|
||||
label={__("IdP Metadata URL (Optional)")}
|
||||
placeholder="https://idp.example.com/metadata.xml"
|
||||
error={form.formState.errors.idpMetadataUrl?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-base font-medium mb-4">
|
||||
{__("Attribute Mapping")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<Field
|
||||
{...form.register("attributeEmail")}
|
||||
label={__("Email Attribute")}
|
||||
placeholder="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
|
||||
error={form.formState.errors.attributeEmail?.message}
|
||||
/>
|
||||
<Field
|
||||
{...form.register("attributeFirstname")}
|
||||
label={__("First Name Attribute")}
|
||||
placeholder="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
|
||||
error={form.formState.errors.attributeFirstname?.message}
|
||||
/>
|
||||
<Field
|
||||
{...form.register("attributeLastname")}
|
||||
label={__("Last Name Attribute")}
|
||||
placeholder="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
|
||||
error={form.formState.errors.attributeLastname?.message}
|
||||
/>
|
||||
<Field
|
||||
{...form.register("attributeRole")}
|
||||
label={__("Role Attribute")}
|
||||
placeholder="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role"
|
||||
error={form.formState.errors.attributeRole?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-base font-medium mb-4">
|
||||
{__("Default Role")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<Field
|
||||
{...form.register("defaultRole")}
|
||||
label={__("Default Role")}
|
||||
placeholder="MEMBER"
|
||||
error={form.formState.errors.defaultRole?.message}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
{__("The IdP must provide roles directly (OWNER, ADMIN, MEMBER, VIEWER). This default role will be used when the role attribute is missing or invalid.")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="autoSignupEnabled"
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
<Label htmlFor="autoSignupEnabled" className="cursor-pointer">
|
||||
{__("Enable automatic user signup via SAML")}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleCloseModal}
|
||||
disabled={isCreating || isUpdating || isInitiating || isVerifying}
|
||||
>
|
||||
{__("Cancel")}
|
||||
</Button>
|
||||
|
||||
{currentStep === "initiate" && (
|
||||
<Button
|
||||
onClick={handleInitiateDomain}
|
||||
disabled={isInitiating}
|
||||
>
|
||||
{__("Next: Verify Domain")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentStep === "verify" && (
|
||||
<Button
|
||||
onClick={handleVerifyDomain}
|
||||
disabled={isVerifying}
|
||||
>
|
||||
{__("Verify and Continue")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentStep === "configure" && (
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
disabled={isCreating || isUpdating}
|
||||
>
|
||||
{editingConfig?.domainVerified ? __("Update Configuration") : __("Create Configuration")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
154
apps/console/src/pages/organizations/settings/__generated__/DomainSettingsTabFragment.graphql.ts
generated
Normal file
154
apps/console/src/pages/organizations/settings/__generated__/DomainSettingsTabFragment.graphql.ts
generated
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @generated SignedSource<<0e2aa976b1c9bb8dddf8b1dcc2f171d1>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
export type SSLStatus = "ACTIVE" | "EXPIRED" | "FAILED" | "PENDING" | "PROVISIONING" | "RENEWING";
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type DomainSettingsTabFragment$data = {
|
||||
readonly customDomain: {
|
||||
readonly createdAt: any;
|
||||
readonly dnsRecords: ReadonlyArray<{
|
||||
readonly name: string;
|
||||
readonly purpose: string;
|
||||
readonly ttl: number;
|
||||
readonly type: string;
|
||||
readonly value: string;
|
||||
}>;
|
||||
readonly domain: string;
|
||||
readonly id: string;
|
||||
readonly sslExpiresAt: any | null | undefined;
|
||||
readonly sslStatus: SSLStatus;
|
||||
readonly updatedAt: any;
|
||||
} | null | undefined;
|
||||
readonly id: string;
|
||||
readonly " $fragmentType": "DomainSettingsTabFragment";
|
||||
};
|
||||
export type DomainSettingsTabFragment$key = {
|
||||
readonly " $data"?: DomainSettingsTabFragment$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"DomainSettingsTabFragment">;
|
||||
};
|
||||
|
||||
const node: ReaderFragment = (function(){
|
||||
var v0 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "DomainSettingsTabFragment",
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "CustomDomain",
|
||||
"kind": "LinkedField",
|
||||
"name": "customDomain",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "domain",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "sslStatus",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "DNSRecordInstruction",
|
||||
"kind": "LinkedField",
|
||||
"name": "dnsRecords",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "type",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "value",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "ttl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "purpose",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "sslExpiresAt",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Organization",
|
||||
"abstractKey": null
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "00306efb96d302284155f5324ba2fb99";
|
||||
|
||||
export default node;
|
||||
114
apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTabFragment.graphql.ts
generated
Normal file
114
apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTabFragment.graphql.ts
generated
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* @generated SignedSource<<02beba57812b8dd7c5c61fb10291de5a>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type GeneralSettingsTabFragment$data = {
|
||||
readonly createdAt: any;
|
||||
readonly description: string | null | undefined;
|
||||
readonly email: string | null | undefined;
|
||||
readonly headquarterAddress: string | null | undefined;
|
||||
readonly horizontalLogoUrl: string | null | undefined;
|
||||
readonly id: string;
|
||||
readonly logoUrl: string | null | undefined;
|
||||
readonly name: string;
|
||||
readonly updatedAt: any;
|
||||
readonly websiteUrl: string | null | undefined;
|
||||
readonly " $fragmentType": "GeneralSettingsTabFragment";
|
||||
};
|
||||
export type GeneralSettingsTabFragment$key = {
|
||||
readonly " $data"?: GeneralSettingsTabFragment$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"GeneralSettingsTabFragment">;
|
||||
};
|
||||
|
||||
const node: ReaderFragment = {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "GeneralSettingsTabFragment",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "logoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "horizontalLogoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "websiteUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "headquarterAddress",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Organization",
|
||||
"abstractKey": null
|
||||
};
|
||||
|
||||
(node as any).hash = "f6552148ce1c0061c4f5cbef78d46c0e";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<4ed3d530d746d8b84b2dba8752e57abc>>
|
||||
* @generated SignedSource<<8e0818a9214ba3613f9d356e61a75e08>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -12,10 +12,10 @@ import { ConcreteRequest } from 'relay-runtime';
|
||||
export type DeleteOrganizationHorizontalLogoInput = {
|
||||
organizationId: string;
|
||||
};
|
||||
export type SettingsPage_DeleteHorizontalLogoMutation$variables = {
|
||||
export type GeneralSettingsTab_DeleteHorizontalLogoMutation$variables = {
|
||||
input: DeleteOrganizationHorizontalLogoInput;
|
||||
};
|
||||
export type SettingsPage_DeleteHorizontalLogoMutation$data = {
|
||||
export type GeneralSettingsTab_DeleteHorizontalLogoMutation$data = {
|
||||
readonly deleteOrganizationHorizontalLogo: {
|
||||
readonly organization: {
|
||||
readonly horizontalLogoUrl: string | null | undefined;
|
||||
@@ -23,9 +23,9 @@ export type SettingsPage_DeleteHorizontalLogoMutation$data = {
|
||||
};
|
||||
};
|
||||
};
|
||||
export type SettingsPage_DeleteHorizontalLogoMutation = {
|
||||
response: SettingsPage_DeleteHorizontalLogoMutation$data;
|
||||
variables: SettingsPage_DeleteHorizontalLogoMutation$variables;
|
||||
export type GeneralSettingsTab_DeleteHorizontalLogoMutation = {
|
||||
response: GeneralSettingsTab_DeleteHorizontalLogoMutation$data;
|
||||
variables: GeneralSettingsTab_DeleteHorizontalLogoMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -85,7 +85,7 @@ return {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SettingsPage_DeleteHorizontalLogoMutation",
|
||||
"name": "GeneralSettingsTab_DeleteHorizontalLogoMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
@@ -94,20 +94,20 @@ return {
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "SettingsPage_DeleteHorizontalLogoMutation",
|
||||
"name": "GeneralSettingsTab_DeleteHorizontalLogoMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "e631480d50c9347050fdc62075a2e3a3",
|
||||
"cacheID": "fbfaf507b48e2ef274e44372a70b3d88",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SettingsPage_DeleteHorizontalLogoMutation",
|
||||
"name": "GeneralSettingsTab_DeleteHorizontalLogoMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SettingsPage_DeleteHorizontalLogoMutation(\n $input: DeleteOrganizationHorizontalLogoInput!\n) {\n deleteOrganizationHorizontalLogo(input: $input) {\n organization {\n id\n horizontalLogoUrl\n }\n }\n}\n"
|
||||
"text": "mutation GeneralSettingsTab_DeleteHorizontalLogoMutation(\n $input: DeleteOrganizationHorizontalLogoInput!\n) {\n deleteOrganizationHorizontalLogo(input: $input) {\n organization {\n id\n horizontalLogoUrl\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "751c3ff44c59511451095ffc66446c2f";
|
||||
(node as any).hash = "7910936d423f99e36ee0a082f5c9336c";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<ac93ed76dd8e53876d200e40f5b62edb>>
|
||||
* @generated SignedSource<<270832e99647c6636914a9644a65358c>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -19,10 +19,10 @@ export type UpdateOrganizationInput = {
|
||||
organizationId: string;
|
||||
websiteUrl?: string | null | undefined;
|
||||
};
|
||||
export type SettingsPage_UpdateMutation$variables = {
|
||||
export type GeneralSettingsTab_UpdateMutation$variables = {
|
||||
input: UpdateOrganizationInput;
|
||||
};
|
||||
export type SettingsPage_UpdateMutation$data = {
|
||||
export type GeneralSettingsTab_UpdateMutation$data = {
|
||||
readonly updateOrganization: {
|
||||
readonly organization: {
|
||||
readonly description: string | null | undefined;
|
||||
@@ -36,9 +36,9 @@ export type SettingsPage_UpdateMutation$data = {
|
||||
};
|
||||
};
|
||||
};
|
||||
export type SettingsPage_UpdateMutation = {
|
||||
response: SettingsPage_UpdateMutation$data;
|
||||
variables: SettingsPage_UpdateMutation$variables;
|
||||
export type GeneralSettingsTab_UpdateMutation = {
|
||||
response: GeneralSettingsTab_UpdateMutation$data;
|
||||
variables: GeneralSettingsTab_UpdateMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -140,7 +140,7 @@ return {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SettingsPage_UpdateMutation",
|
||||
"name": "GeneralSettingsTab_UpdateMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
@@ -149,20 +149,20 @@ return {
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "SettingsPage_UpdateMutation",
|
||||
"name": "GeneralSettingsTab_UpdateMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "097a6e519249d6f2b3c2a95963f85a6c",
|
||||
"cacheID": "55c07b334317a5ca30023ef5354a6c45",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SettingsPage_UpdateMutation",
|
||||
"name": "GeneralSettingsTab_UpdateMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SettingsPage_UpdateMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n }\n }\n}\n"
|
||||
"text": "mutation GeneralSettingsTab_UpdateMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "c676129018636d84dbca9d8c98962af7";
|
||||
(node as any).hash = "f1731adcb4bf7b7214301a612f48567e";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<7ca4537a89a599fe5d2b7ecad753aa64>>
|
||||
* @generated SignedSource<<1077fac0cf9631664adf53347f21a5b0>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -11,7 +11,7 @@
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
export type InvitationStatus = "ACCEPTED" | "EXPIRED" | "PENDING";
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type SettingsPageInvitationsFragment$data = {
|
||||
export type MembersSettingsTabInvitationsFragment$data = {
|
||||
readonly id: string;
|
||||
readonly invitations: {
|
||||
readonly __id: string;
|
||||
@@ -29,14 +29,14 @@ export type SettingsPageInvitationsFragment$data = {
|
||||
}>;
|
||||
readonly totalCount: number;
|
||||
};
|
||||
readonly " $fragmentType": "SettingsPageInvitationsFragment";
|
||||
readonly " $fragmentType": "MembersSettingsTabInvitationsFragment";
|
||||
};
|
||||
export type SettingsPageInvitationsFragment$key = {
|
||||
readonly " $data"?: SettingsPageInvitationsFragment$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageInvitationsFragment">;
|
||||
export type MembersSettingsTabInvitationsFragment$key = {
|
||||
readonly " $data"?: MembersSettingsTabInvitationsFragment$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"MembersSettingsTabInvitationsFragment">;
|
||||
};
|
||||
|
||||
import SettingsInvitationsRefetchQuery_graphql from './SettingsInvitationsRefetchQuery.graphql';
|
||||
import MembersSettingsTabInvitationsRefetchQuery_graphql from './MembersSettingsTabInvitationsRefetchQuery.graphql';
|
||||
|
||||
const node: ReaderFragment = (function(){
|
||||
var v0 = [
|
||||
@@ -105,28 +105,18 @@ return {
|
||||
"fragmentPathInResult": [
|
||||
"node"
|
||||
],
|
||||
"operation": SettingsInvitationsRefetchQuery_graphql,
|
||||
"operation": MembersSettingsTabInvitationsRefetchQuery_graphql,
|
||||
"identifierInfo": {
|
||||
"identifierField": "id",
|
||||
"identifierQueryVariableName": "id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "SettingsPageInvitationsFragment",
|
||||
"name": "MembersSettingsTabInvitationsFragment",
|
||||
"selections": [
|
||||
{
|
||||
"alias": "invitations",
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"statuses": [
|
||||
"PENDING",
|
||||
"EXPIRED"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "orderBy",
|
||||
@@ -135,7 +125,7 @@ return {
|
||||
],
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "__SettingsPageInvitations_invitations_connection",
|
||||
"name": "__MembersSettingsTabInvitations_invitations_connection",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
@@ -166,14 +156,14 @@ return {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
@@ -190,6 +180,13 @@ return {
|
||||
"name": "status",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -204,13 +201,6 @@ return {
|
||||
"name": "acceptedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -292,6 +282,6 @@ return {
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "b56157db731d3968bb825dd36375ddb7";
|
||||
(node as any).hash = "632fb80f7f536c576adaef2ec4007588";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<39e11ee9cf99dc541aa5a8df259e8438>>
|
||||
* @generated SignedSource<<24e16bb5ea83a195376f3356801ecdd6>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -16,7 +16,7 @@ export type InvitationOrder = {
|
||||
direction: OrderDirection;
|
||||
field: InvitationOrderField;
|
||||
};
|
||||
export type SettingsInvitationsRefetchQuery$variables = {
|
||||
export type MembersSettingsTabInvitationsRefetchQuery$variables = {
|
||||
after?: any | null | undefined;
|
||||
before?: any | null | undefined;
|
||||
first?: number | null | undefined;
|
||||
@@ -24,14 +24,14 @@ export type SettingsInvitationsRefetchQuery$variables = {
|
||||
last?: number | null | undefined;
|
||||
order?: InvitationOrder | null | undefined;
|
||||
};
|
||||
export type SettingsInvitationsRefetchQuery$data = {
|
||||
export type MembersSettingsTabInvitationsRefetchQuery$data = {
|
||||
readonly node: {
|
||||
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageInvitationsFragment">;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"MembersSettingsTabInvitationsFragment">;
|
||||
};
|
||||
};
|
||||
export type SettingsInvitationsRefetchQuery = {
|
||||
response: SettingsInvitationsRefetchQuery$data;
|
||||
variables: SettingsInvitationsRefetchQuery$variables;
|
||||
export type MembersSettingsTabInvitationsRefetchQuery = {
|
||||
response: MembersSettingsTabInvitationsRefetchQuery$data;
|
||||
variables: MembersSettingsTabInvitationsRefetchQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -112,16 +112,6 @@ v12 = {
|
||||
v13 = [
|
||||
(v7/*: any*/),
|
||||
(v8/*: any*/),
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"statuses": [
|
||||
"PENDING",
|
||||
"EXPIRED"
|
||||
]
|
||||
}
|
||||
},
|
||||
(v9/*: any*/),
|
||||
(v10/*: any*/),
|
||||
{
|
||||
@@ -142,7 +132,7 @@ return {
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SettingsInvitationsRefetchQuery",
|
||||
"name": "MembersSettingsTabInvitationsRefetchQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -165,7 +155,7 @@ return {
|
||||
}
|
||||
],
|
||||
"kind": "FragmentSpread",
|
||||
"name": "SettingsPageInvitationsFragment"
|
||||
"name": "MembersSettingsTabInvitationsFragment"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -185,7 +175,7 @@ return {
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "SettingsInvitationsRefetchQuery",
|
||||
"name": "MembersSettingsTabInvitationsRefetchQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -236,14 +226,14 @@ return {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
@@ -260,6 +250,13 @@ return {
|
||||
"name": "status",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -274,13 +271,6 @@ return {
|
||||
"name": "acceptedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
(v11/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -353,11 +343,10 @@ return {
|
||||
"alias": null,
|
||||
"args": (v13/*: any*/),
|
||||
"filters": [
|
||||
"orderBy",
|
||||
"filter"
|
||||
"orderBy"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "SettingsPageInvitations_invitations",
|
||||
"key": "MembersSettingsTabInvitations_invitations",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "invitations"
|
||||
}
|
||||
@@ -371,16 +360,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "24b42a44e783a7f8499a568de2c8f9f4",
|
||||
"cacheID": "e6105642c2d4bc7c1023e4456cebff8e",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SettingsInvitationsRefetchQuery",
|
||||
"name": "MembersSettingsTabInvitationsRefetchQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query SettingsInvitationsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: InvitationOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...SettingsPageInvitationsFragment_16fISc\n id\n }\n}\n\nfragment SettingsPageInvitationsFragment_16fISc on Organization {\n invitations(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: {statuses: [PENDING, EXPIRED]}) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n status\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
|
||||
"text": "query MembersSettingsTabInvitationsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: InvitationOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MembersSettingsTabInvitationsFragment_16fISc\n id\n }\n}\n\nfragment MembersSettingsTabInvitationsFragment_16fISc on Organization {\n invitations(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n fullName\n email\n role\n status\n createdAt\n expiresAt\n acceptedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "b56157db731d3968bb825dd36375ddb7";
|
||||
(node as any).hash = "632fb80f7f536c576adaef2ec4007588";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<778a6585d3f3d9b346273c20319ce96d>>
|
||||
* @generated SignedSource<<f4c0ddd2099746a005858cecc39d2a23>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -9,13 +9,15 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
export type UserAuthMethod = "PASSWORD" | "SAML";
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type SettingsPageMembershipsFragment$data = {
|
||||
export type MembersSettingsTabMembershipsFragment$data = {
|
||||
readonly id: string;
|
||||
readonly memberships: {
|
||||
readonly __id: string;
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly authMethod: UserAuthMethod;
|
||||
readonly createdAt: any;
|
||||
readonly emailAddress: string;
|
||||
readonly fullName: string;
|
||||
@@ -25,14 +27,14 @@ export type SettingsPageMembershipsFragment$data = {
|
||||
}>;
|
||||
readonly totalCount: number;
|
||||
};
|
||||
readonly " $fragmentType": "SettingsPageMembershipsFragment";
|
||||
readonly " $fragmentType": "MembersSettingsTabMembershipsFragment";
|
||||
};
|
||||
export type SettingsPageMembershipsFragment$key = {
|
||||
readonly " $data"?: SettingsPageMembershipsFragment$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageMembershipsFragment">;
|
||||
export type MembersSettingsTabMembershipsFragment$key = {
|
||||
readonly " $data"?: MembersSettingsTabMembershipsFragment$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"MembersSettingsTabMembershipsFragment">;
|
||||
};
|
||||
|
||||
import SettingsMembershipsRefetchQuery_graphql from './SettingsMembershipsRefetchQuery.graphql';
|
||||
import MembersSettingsTabMembershipsRefetchQuery_graphql from './MembersSettingsTabMembershipsRefetchQuery.graphql';
|
||||
|
||||
const node: ReaderFragment = (function(){
|
||||
var v0 = [
|
||||
@@ -101,14 +103,14 @@ return {
|
||||
"fragmentPathInResult": [
|
||||
"node"
|
||||
],
|
||||
"operation": SettingsMembershipsRefetchQuery_graphql,
|
||||
"operation": MembersSettingsTabMembershipsRefetchQuery_graphql,
|
||||
"identifierInfo": {
|
||||
"identifierField": "id",
|
||||
"identifierQueryVariableName": "id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "SettingsPageMembershipsFragment",
|
||||
"name": "MembersSettingsTabMembershipsFragment",
|
||||
"selections": [
|
||||
{
|
||||
"alias": "memberships",
|
||||
@@ -121,7 +123,7 @@ return {
|
||||
],
|
||||
"concreteType": "MembershipConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "__SettingsPageMemberships_memberships_connection",
|
||||
"name": "__MembersSettingsTabMemberships_memberships_connection",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
@@ -169,6 +171,13 @@ return {
|
||||
"name": "role",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "authMethod",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -257,6 +266,6 @@ return {
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "d28514cdb5181fc1023dc4ea5bddb4f2";
|
||||
(node as any).hash = "c9e341e99052ba74299c5ddd0433d7c0";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<49986e7498d757f2074e93e6cf4747a5>>
|
||||
* @generated SignedSource<<b99cb7c099e133b550b2467abbe270e8>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -16,7 +16,7 @@ export type MembershipOrder = {
|
||||
direction: OrderDirection;
|
||||
field: MembershipOrderField;
|
||||
};
|
||||
export type SettingsMembershipsRefetchQuery$variables = {
|
||||
export type MembersSettingsTabMembershipsRefetchQuery$variables = {
|
||||
after?: any | null | undefined;
|
||||
before?: any | null | undefined;
|
||||
first?: number | null | undefined;
|
||||
@@ -24,14 +24,14 @@ export type SettingsMembershipsRefetchQuery$variables = {
|
||||
last?: number | null | undefined;
|
||||
order?: MembershipOrder | null | undefined;
|
||||
};
|
||||
export type SettingsMembershipsRefetchQuery$data = {
|
||||
export type MembersSettingsTabMembershipsRefetchQuery$data = {
|
||||
readonly node: {
|
||||
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageMembershipsFragment">;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"MembersSettingsTabMembershipsFragment">;
|
||||
};
|
||||
};
|
||||
export type SettingsMembershipsRefetchQuery = {
|
||||
response: SettingsMembershipsRefetchQuery$data;
|
||||
variables: SettingsMembershipsRefetchQuery$variables;
|
||||
export type MembersSettingsTabMembershipsRefetchQuery = {
|
||||
response: MembersSettingsTabMembershipsRefetchQuery$data;
|
||||
variables: MembersSettingsTabMembershipsRefetchQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -132,7 +132,7 @@ return {
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SettingsMembershipsRefetchQuery",
|
||||
"name": "MembersSettingsTabMembershipsRefetchQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -155,7 +155,7 @@ return {
|
||||
}
|
||||
],
|
||||
"kind": "FragmentSpread",
|
||||
"name": "SettingsPageMembershipsFragment"
|
||||
"name": "MembersSettingsTabMembershipsFragment"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -175,7 +175,7 @@ return {
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "SettingsMembershipsRefetchQuery",
|
||||
"name": "MembersSettingsTabMembershipsRefetchQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -243,6 +243,13 @@ return {
|
||||
"name": "role",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "authMethod",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -325,7 +332,7 @@ return {
|
||||
"orderBy"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "SettingsPageMemberships_memberships",
|
||||
"key": "MembersSettingsTabMemberships_memberships",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "memberships"
|
||||
}
|
||||
@@ -339,16 +346,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "57c4ca08006166b58c1fb2407f091704",
|
||||
"cacheID": "27627ac3e1ea017ee7783fcfd502c0f8",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SettingsMembershipsRefetchQuery",
|
||||
"name": "MembersSettingsTabMembershipsRefetchQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query SettingsMembershipsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MembershipOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...SettingsPageMembershipsFragment_16fISc\n id\n }\n}\n\nfragment SettingsPageMembershipsFragment_16fISc on Organization {\n memberships(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
|
||||
"text": "query MembersSettingsTabMembershipsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MembershipOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MembersSettingsTabMembershipsFragment_16fISc\n id\n }\n}\n\nfragment MembersSettingsTabMembershipsFragment_16fISc on Organization {\n memberships(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n authMethod\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "d28514cdb5181fc1023dc4ea5bddb4f2";
|
||||
(node as any).hash = "c9e341e99052ba74299c5ddd0433d7c0";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<f058193b755f9fed3efe4910aae9a7e3>>
|
||||
* @generated SignedSource<<057a86325a80ac7377b18a50896f01e1>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -12,18 +12,18 @@ import { ConcreteRequest } from 'relay-runtime';
|
||||
export type DeleteInvitationInput = {
|
||||
invitationId: string;
|
||||
};
|
||||
export type SettingsPage_DeleteInvitationMutation$variables = {
|
||||
export type MembersSettingsTab_DeleteInvitationMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: DeleteInvitationInput;
|
||||
};
|
||||
export type SettingsPage_DeleteInvitationMutation$data = {
|
||||
export type MembersSettingsTab_DeleteInvitationMutation$data = {
|
||||
readonly deleteInvitation: {
|
||||
readonly deletedInvitationId: string;
|
||||
};
|
||||
};
|
||||
export type SettingsPage_DeleteInvitationMutation = {
|
||||
response: SettingsPage_DeleteInvitationMutation$data;
|
||||
variables: SettingsPage_DeleteInvitationMutation$variables;
|
||||
export type MembersSettingsTab_DeleteInvitationMutation = {
|
||||
response: MembersSettingsTab_DeleteInvitationMutation$data;
|
||||
variables: MembersSettingsTab_DeleteInvitationMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -59,7 +59,7 @@ return {
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SettingsPage_DeleteInvitationMutation",
|
||||
"name": "MembersSettingsTab_DeleteInvitationMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -84,7 +84,7 @@ return {
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "SettingsPage_DeleteInvitationMutation",
|
||||
"name": "MembersSettingsTab_DeleteInvitationMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -117,16 +117,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "1c362c5db7a985d7548166b2b1eb42c9",
|
||||
"cacheID": "c995f13c967dab141f64f9ad6314f3a2",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SettingsPage_DeleteInvitationMutation",
|
||||
"name": "MembersSettingsTab_DeleteInvitationMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SettingsPage_DeleteInvitationMutation(\n $input: DeleteInvitationInput!\n) {\n deleteInvitation(input: $input) {\n deletedInvitationId\n }\n}\n"
|
||||
"text": "mutation MembersSettingsTab_DeleteInvitationMutation(\n $input: DeleteInvitationInput!\n) {\n deleteInvitation(input: $input) {\n deletedInvitationId\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "3c484508ba04b5a75eca62fa6afeb16d";
|
||||
(node as any).hash = "ad47509295919c7f0e6ff7895777231f";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<d210161405099a17aa25d06ca4563634>>
|
||||
* @generated SignedSource<<b5471d97fe00df7ef4c16669ebc89916>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -13,18 +13,18 @@ export type RemoveMemberInput = {
|
||||
memberId: string;
|
||||
organizationId: string;
|
||||
};
|
||||
export type SettingsPage_RemoveMemberMutation$variables = {
|
||||
export type MembersSettingsTab_RemoveMemberMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: RemoveMemberInput;
|
||||
};
|
||||
export type SettingsPage_RemoveMemberMutation$data = {
|
||||
export type MembersSettingsTab_RemoveMemberMutation$data = {
|
||||
readonly removeMember: {
|
||||
readonly deletedMemberId: string;
|
||||
};
|
||||
};
|
||||
export type SettingsPage_RemoveMemberMutation = {
|
||||
response: SettingsPage_RemoveMemberMutation$data;
|
||||
variables: SettingsPage_RemoveMemberMutation$variables;
|
||||
export type MembersSettingsTab_RemoveMemberMutation = {
|
||||
response: MembersSettingsTab_RemoveMemberMutation$data;
|
||||
variables: MembersSettingsTab_RemoveMemberMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -60,7 +60,7 @@ return {
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SettingsPage_RemoveMemberMutation",
|
||||
"name": "MembersSettingsTab_RemoveMemberMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -85,7 +85,7 @@ return {
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "SettingsPage_RemoveMemberMutation",
|
||||
"name": "MembersSettingsTab_RemoveMemberMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -118,16 +118,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "e2dd0f4d7327ce3bc97754c85d3f700d",
|
||||
"cacheID": "6ccac45c6bedfbfe98b6c6344ea5df28",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SettingsPage_RemoveMemberMutation",
|
||||
"name": "MembersSettingsTab_RemoveMemberMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SettingsPage_RemoveMemberMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n deletedMemberId\n }\n}\n"
|
||||
"text": "mutation MembersSettingsTab_RemoveMemberMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n deletedMemberId\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "9909a8b95f8d8621ffdf02da34ec8da2";
|
||||
(node as any).hash = "97f72349476066a0de4e580d2e4e1b0e";
|
||||
|
||||
export default node;
|
||||
229
apps/console/src/pages/organizations/settings/__generated__/SAMLSettingsTabFragment.graphql.ts
generated
Normal file
229
apps/console/src/pages/organizations/settings/__generated__/SAMLSettingsTabFragment.graphql.ts
generated
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* @generated SignedSource<<0b3ea1127a3a6388e6d0802abea0307c>>
|
||||
* @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 SAMLSettingsTabFragment$data = {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly samlConfigurations: ReadonlyArray<{
|
||||
readonly attributeEmail: string;
|
||||
readonly attributeFirstname: string;
|
||||
readonly attributeLastname: string;
|
||||
readonly attributeRole: string;
|
||||
readonly autoSignupEnabled: boolean;
|
||||
readonly defaultRole: string;
|
||||
readonly domainVerificationToken: string | null | undefined;
|
||||
readonly domainVerified: boolean;
|
||||
readonly domainVerifiedAt: any | null | undefined;
|
||||
readonly emailDomain: string;
|
||||
readonly enabled: boolean;
|
||||
readonly enforcementPolicy: SAMLEnforcementPolicy;
|
||||
readonly id: string;
|
||||
readonly idpCertificate: string;
|
||||
readonly idpEntityId: string;
|
||||
readonly idpMetadataUrl: string | null | undefined;
|
||||
readonly idpSsoUrl: string;
|
||||
readonly spAcsUrl: string;
|
||||
readonly spEntityId: string;
|
||||
readonly spMetadataUrl: string;
|
||||
readonly testLoginUrl: string;
|
||||
}>;
|
||||
readonly " $fragmentType": "SAMLSettingsTabFragment";
|
||||
};
|
||||
export type SAMLSettingsTabFragment$key = {
|
||||
readonly " $data"?: SAMLSettingsTabFragment$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"SAMLSettingsTabFragment">;
|
||||
};
|
||||
|
||||
const node: ReaderFragment = (function(){
|
||||
var v0 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SAMLSettingsTabFragment",
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "SAMLConfiguration",
|
||||
"kind": "LinkedField",
|
||||
"name": "samlConfigurations",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "enabled",
|
||||
"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": "domainVerified",
|
||||
"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": "spEntityId",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "spAcsUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "spMetadataUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "testLoginUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpEntityId",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpSsoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpCertificate",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "idpMetadataUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeEmail",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeFirstname",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeLastname",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "attributeRole",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "defaultRole",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "autoSignupEnabled",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Organization",
|
||||
"abstractKey": null
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "691298e053f77bcb6ef13a5869a86579";
|
||||
|
||||
export default node;
|
||||
@@ -23,6 +23,27 @@ export class InternalServerError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthenticationRequiredError extends Error {
|
||||
public redirectUrl: string;
|
||||
public requiresSaml: boolean;
|
||||
public organizationId: string;
|
||||
public samlConfigId?: string;
|
||||
|
||||
constructor(extensions: {
|
||||
redirectUrl: string;
|
||||
requiresSaml: boolean;
|
||||
organizationId: string;
|
||||
samlConfigId?: string;
|
||||
}) {
|
||||
super("AUTHENTICATION_REQUIRED");
|
||||
this.name = "AuthenticationRequiredError";
|
||||
this.redirectUrl = extensions.redirectUrl;
|
||||
this.requiresSaml = extensions.requiresSaml;
|
||||
this.organizationId = extensions.organizationId;
|
||||
this.samlConfigId = extensions.samlConfigId;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildEndpoint(path: string): string {
|
||||
const host = import.meta.env.VITE_API_URL;
|
||||
|
||||
@@ -47,6 +68,9 @@ export function buildEndpoint(path: string): string {
|
||||
const hasUnauthenticatedError = (error: GraphQLError) =>
|
||||
error.extensions?.code == "UNAUTHENTICATED";
|
||||
|
||||
const hasAuthenticationRequiredError = (error: GraphQLError) =>
|
||||
error.extensions?.code == "AUTHENTICATION_REQUIRED";
|
||||
|
||||
const fetchRelay: FetchFunction = async (
|
||||
request,
|
||||
variables,
|
||||
@@ -117,6 +141,20 @@ const fetchRelay: FetchFunction = async (
|
||||
throw new UnAuthenticatedError();
|
||||
}
|
||||
|
||||
// Check for authentication required errors
|
||||
const authRequiredError = errors.find(hasAuthenticationRequiredError);
|
||||
if (authRequiredError?.extensions) {
|
||||
const { redirectUrl, requiresSaml, organizationId, samlConfigId } = authRequiredError.extensions;
|
||||
|
||||
// Throw the error with all the redirect information
|
||||
throw new AuthenticationRequiredError({
|
||||
redirectUrl: redirectUrl as string,
|
||||
requiresSaml: requiresSaml as boolean,
|
||||
organizationId: organizationId as string,
|
||||
samlConfigId: samlConfigId as string | undefined,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Error fetching GraphQL query '${
|
||||
request.name
|
||||
|
||||
@@ -50,7 +50,7 @@ function ErrorBoundary({ error: propsError }: { error?: string }) {
|
||||
const error = useRouteError() ?? propsError;
|
||||
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
return <Navigate to="/auth/login" />;
|
||||
return <Navigate to="/authentication/login" />;
|
||||
}
|
||||
|
||||
return <PageError error={error?.toString()} />;
|
||||
@@ -58,7 +58,7 @@ function ErrorBoundary({ error: propsError }: { error?: string }) {
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/auth",
|
||||
path: "/authentication",
|
||||
Component: AuthLayout,
|
||||
children: [
|
||||
{
|
||||
@@ -131,6 +131,30 @@ const routes = [
|
||||
organizationId,
|
||||
}),
|
||||
Component: lazy(() => import("./pages/organizations/SettingsPage")),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
loader: () => {
|
||||
throw redirect("general");
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "general",
|
||||
Component: lazy(() => import("./pages/organizations/settings/GeneralSettingsTab")),
|
||||
},
|
||||
{
|
||||
path: "members",
|
||||
Component: lazy(() => import("./pages/organizations/settings/MembersSettingsTab")),
|
||||
},
|
||||
{
|
||||
path: "domain",
|
||||
Component: lazy(() => import("./pages/organizations/settings/DomainSettingsTab")),
|
||||
},
|
||||
{
|
||||
path: "saml-sso",
|
||||
Component: lazy(() => import("./pages/organizations/settings/SAMLSettingsTab")),
|
||||
},
|
||||
],
|
||||
},
|
||||
...riskRoutes,
|
||||
...measureRoutes,
|
||||
|
||||
Reference in New Issue
Block a user