Read OAuth2 scopes from GraphQL on the connector frontend
Each component that initiates an OAuth2 flow now reads its scopes from the colocated Relay fragment instead of a hardcoded TypeScript map. The five live call sites pass scopes to the backend via the new ?scope= query parameter. Drop the dead CreateAccessSourceDialog React component and rename the file to accessSourceMutations.ts since only the mutation export was used. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import { GoogleWorkspaceConnector } from "./GoogleWorkspaceConnector";
|
||||
|
||||
const connectorListFragment = graphql`
|
||||
fragment ConnectorListFragment on Organization {
|
||||
googleWorkspaceOAuth2Scopes
|
||||
scimConfiguration {
|
||||
...GoogleWorkspaceConnectorFragment
|
||||
}
|
||||
@@ -40,7 +41,10 @@ export function ConnectorList(props: { fKey: ConnectorListFragment$key }) {
|
||||
"Connect your identity provider to automatically sync users to your organization. Once connected, you don't need to configure SCIM manually.",
|
||||
)}
|
||||
</p>
|
||||
<GoogleWorkspaceConnector fKey={data.scimConfiguration ?? null} />
|
||||
<GoogleWorkspaceConnector
|
||||
fKey={data.scimConfiguration ?? null}
|
||||
oauth2Scopes={data.googleWorkspaceOAuth2Scopes}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,8 +74,9 @@ const updateSCIMBridgeMutation = graphql`
|
||||
|
||||
export function GoogleWorkspaceConnector(props: {
|
||||
fKey: GoogleWorkspaceConnectorFragment$key | null;
|
||||
oauth2Scopes: readonly string[];
|
||||
}) {
|
||||
const { fKey } = props;
|
||||
const { fKey, oauth2Scopes } = props;
|
||||
const data = useFragment<GoogleWorkspaceConnectorFragment$key>(googleWorkspaceConnectorFragment, fKey);
|
||||
const bridge = data?.bridge;
|
||||
const connector = bridge?.connector;
|
||||
@@ -112,6 +113,9 @@ export function GoogleWorkspaceConnector(props: {
|
||||
const url = new URL("/api/console/v1/connectors/initiate", baseUrl);
|
||||
url.searchParams.append("organization_id", organizationId);
|
||||
url.searchParams.append("provider", "GOOGLE_WORKSPACE");
|
||||
for (const scope of oauth2Scopes) {
|
||||
url.searchParams.append("scope", scope);
|
||||
}
|
||||
const continueUrl = `/organizations/${organizationId}/settings/scim`;
|
||||
url.searchParams.append("continue", continueUrl);
|
||||
window.location.href = url.toString();
|
||||
|
||||
@@ -27,12 +27,12 @@ import { Link, useNavigate } from "react-router";
|
||||
import { ConnectionHandler, graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql";
|
||||
import type { accessSourceMutationsCreateMutation } from "#/__generated__/core/accessSourceMutationsCreateMutation.graphql";
|
||||
import type { CreateCsvAccessSourcePageQuery } from "#/__generated__/core/CreateCsvAccessSourcePageQuery.graphql";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
import { createAccessSourceMutation } from "./dialogs/CreateAccessSourceDialog";
|
||||
import { createAccessSourceMutation } from "./dialogs/accessSourceMutations";
|
||||
|
||||
export const createCsvAccessSourcePageQuery = graphql`
|
||||
query CreateCsvAccessSourcePageQuery($organizationId: ID!) {
|
||||
@@ -81,7 +81,7 @@ export default function CreateCsvAccessSourcePage({
|
||||
);
|
||||
|
||||
const [createAccessSource, isCreating]
|
||||
= useMutation<CreateAccessSourceDialogMutation>(
|
||||
= useMutation<accessSourceMutationsCreateMutation>(
|
||||
createAccessSourceMutation,
|
||||
);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ const fragment = graphql`
|
||||
connector {
|
||||
provider
|
||||
}
|
||||
oauth2Scopes
|
||||
connectionStatus
|
||||
selectedOrganization
|
||||
needsConfiguration
|
||||
@@ -213,6 +214,9 @@ export function AccessSourceRow({ fKey, connectionId, organizationId }: Props) {
|
||||
url.searchParams.append("organization_id", organizationId);
|
||||
url.searchParams.append("provider", provider);
|
||||
url.searchParams.append("connector_id", accessSource.connectorId);
|
||||
for (const scope of accessSource.oauth2Scopes) {
|
||||
url.searchParams.append("scope", scope);
|
||||
}
|
||||
url.searchParams.append(
|
||||
"continue",
|
||||
`/organizations/${organizationId}/access-reviews/sources`,
|
||||
|
||||
@@ -40,9 +40,9 @@ import { graphql } from "relay-runtime";
|
||||
import type { AddAccessSourceDialogConnectorProviderInfoFragment$data } from "#/__generated__/core/AddAccessSourceDialogConnectorProviderInfoFragment.graphql";
|
||||
import type { AddAccessSourceDialogCreateAPIKeyConnectorMutation } from "#/__generated__/core/AddAccessSourceDialogCreateAPIKeyConnectorMutation.graphql";
|
||||
import type { AddAccessSourceDialogCreateClientCredentialsConnectorMutation } from "#/__generated__/core/AddAccessSourceDialogCreateClientCredentialsConnectorMutation.graphql";
|
||||
import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql";
|
||||
import type { accessSourceMutationsCreateMutation } from "#/__generated__/core/accessSourceMutationsCreateMutation.graphql";
|
||||
|
||||
import { createAccessSourceMutation } from "./CreateAccessSourceDialog";
|
||||
import { createAccessSourceMutation } from "./accessSourceMutations";
|
||||
|
||||
export const addAccessSourceDialogConnectorProviderInfoFragment = graphql`
|
||||
fragment AddAccessSourceDialogConnectorProviderInfoFragment on ConnectorProviderInfo @relay(plural: true) {
|
||||
@@ -51,6 +51,7 @@ export const addAccessSourceDialogConnectorProviderInfoFragment = graphql`
|
||||
oauthConfigured
|
||||
apiKeySupported
|
||||
clientCredentialsSupported
|
||||
oauth2Scopes
|
||||
extraSettings {
|
||||
key
|
||||
label
|
||||
@@ -185,7 +186,7 @@ export function AddAccessSourceDialog({
|
||||
);
|
||||
|
||||
const [createAccessSource]
|
||||
= useMutation<CreateAccessSourceDialogMutation>(
|
||||
= useMutation<accessSourceMutationsCreateMutation>(
|
||||
createAccessSourceMutation,
|
||||
);
|
||||
const [createAPIKeyConnector]
|
||||
@@ -197,11 +198,14 @@ export function AddAccessSourceDialog({
|
||||
createClientCredentialsConnectorMutation,
|
||||
);
|
||||
|
||||
const connectOAuthProvider = (provider: string) => {
|
||||
const connectOAuthProvider = (info: ProviderInfo) => {
|
||||
const baseURL = import.meta.env.VITE_API_URL || window.location.origin;
|
||||
const url = new URL("/api/console/v1/connectors/initiate", baseURL);
|
||||
url.searchParams.append("organization_id", organizationId);
|
||||
url.searchParams.append("provider", provider);
|
||||
url.searchParams.append("provider", info.provider);
|
||||
for (const scope of info.oauth2Scopes) {
|
||||
url.searchParams.append("scope", scope);
|
||||
}
|
||||
url.searchParams.append(
|
||||
"continue",
|
||||
`/organizations/${organizationId}/access-reviews/sources`,
|
||||
@@ -419,7 +423,7 @@ export function AddAccessSourceDialog({
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => connectOAuthProvider(info.provider)}
|
||||
onClick={() => connectOAuthProvider(info)}
|
||||
>
|
||||
{__("Connect")}
|
||||
</Button>
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Field,
|
||||
Option,
|
||||
Select,
|
||||
useDialogRef,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { type ReactNode, useEffect, useMemo } from "react";
|
||||
import { Controller, useWatch } from "react-hook-form";
|
||||
import { graphql, useMutation } from "react-relay";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
|
||||
export const createAccessSourceMutation = graphql`
|
||||
mutation CreateAccessSourceDialogMutation(
|
||||
$input: CreateAccessSourceInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
createAccessSource(input: $input) {
|
||||
accessSourceEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
...AccessSourceRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
organizationId: string;
|
||||
connectionId: string;
|
||||
connectors: ReadonlyArray<{
|
||||
readonly id: string;
|
||||
readonly provider: "GOOGLE_WORKSPACE" | "LINEAR" | "SLACK";
|
||||
readonly createdAt: string;
|
||||
}>;
|
||||
preselectedConnectorId: string | null;
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1),
|
||||
sourceType: z.enum(["CSV", "OAUTH2"]),
|
||||
provider: z.enum(["GOOGLE_WORKSPACE", "LINEAR", "SLACK"]).optional(),
|
||||
connectorId: z.string().optional(),
|
||||
csvData: z.string().optional(),
|
||||
}).superRefine((data, ctx) => {
|
||||
if (data.sourceType === "CSV" && !data.csvData?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["csvData"],
|
||||
message: "CSV data is required for CSV sources.",
|
||||
});
|
||||
}
|
||||
|
||||
if (data.sourceType === "OAUTH2") {
|
||||
if (!data.provider) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["provider"],
|
||||
message: "Provider is required for OAuth2 sources.",
|
||||
});
|
||||
}
|
||||
if (!data.connectorId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["connectorId"],
|
||||
message: "Connector is required for OAuth2 sources.",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function providerLabel(provider: "GOOGLE_WORKSPACE" | "LINEAR" | "SLACK") {
|
||||
switch (provider) {
|
||||
case "GOOGLE_WORKSPACE":
|
||||
return "Google Workspace";
|
||||
case "LINEAR":
|
||||
return "Linear";
|
||||
case "SLACK":
|
||||
return "Slack";
|
||||
default:
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
export function CreateAccessSourceDialog({
|
||||
children,
|
||||
organizationId,
|
||||
connectionId,
|
||||
connectors,
|
||||
preselectedConnectorId,
|
||||
}: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const preselectedConnector = useMemo(
|
||||
() => connectors.find(connector => connector.id === preselectedConnectorId),
|
||||
[connectors, preselectedConnectorId],
|
||||
);
|
||||
const { control, register, handleSubmit, reset, setValue }
|
||||
= useFormWithSchema(
|
||||
schema,
|
||||
{
|
||||
defaultValues: {
|
||||
name: "",
|
||||
sourceType: preselectedConnector ? "OAUTH2" : "CSV",
|
||||
provider: preselectedConnector?.provider ?? "GOOGLE_WORKSPACE",
|
||||
connectorId: preselectedConnector?.id,
|
||||
csvData: "",
|
||||
},
|
||||
},
|
||||
);
|
||||
const sourceType = useWatch({ control, name: "sourceType" });
|
||||
const provider = useWatch({ control, name: "provider" });
|
||||
const connectorId = useWatch({ control, name: "connectorId" });
|
||||
const ref = useDialogRef();
|
||||
|
||||
const providerConnectors = useMemo(
|
||||
() => connectors,
|
||||
[connectors],
|
||||
);
|
||||
const selectableConnectors = useMemo(
|
||||
() =>
|
||||
providerConnectors.filter(
|
||||
connector => !provider || connector.provider === provider,
|
||||
),
|
||||
[provider, providerConnectors],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider) {
|
||||
setValue("connectorId", undefined);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
connectorId
|
||||
&& !selectableConnectors.some(connector => connector.id === connectorId)
|
||||
) {
|
||||
setValue("connectorId", undefined);
|
||||
}
|
||||
}, [provider, connectorId, selectableConnectors, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!preselectedConnector) return;
|
||||
setValue("sourceType", "OAUTH2");
|
||||
setValue("provider", preselectedConnector.provider);
|
||||
setValue("connectorId", preselectedConnector.id);
|
||||
}, [preselectedConnector, setValue]);
|
||||
|
||||
const [createAccessSource, isCreating]
|
||||
= useMutation<CreateAccessSourceDialogMutation>(
|
||||
createAccessSourceMutation,
|
||||
);
|
||||
|
||||
const clearConnectorQueryParam = () => {
|
||||
if (!searchParams.get("connector_id")) {
|
||||
return;
|
||||
}
|
||||
setSearchParams((params) => {
|
||||
params.delete("connector_id");
|
||||
return params;
|
||||
});
|
||||
};
|
||||
|
||||
const startOAuthConnection = () => {
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseURL = import.meta.env.VITE_API_URL || window.location.origin;
|
||||
const url = new URL("/api/console/v1/connectors/initiate", baseURL);
|
||||
url.searchParams.append("organization_id", organizationId);
|
||||
url.searchParams.append("provider", provider);
|
||||
url.searchParams.append("continue", `/organizations/${organizationId}/access-reviews`);
|
||||
window.location.href = url.toString();
|
||||
};
|
||||
|
||||
const onSubmit = (data: z.infer<typeof schema>) => {
|
||||
const isOAuth = data.sourceType === "OAUTH2";
|
||||
createAccessSource({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId,
|
||||
connectorId: isOAuth ? data.connectorId : null,
|
||||
name: data.name,
|
||||
csvData: isOAuth ? null : (data.csvData || null),
|
||||
},
|
||||
connections: [connectionId],
|
||||
},
|
||||
onCompleted(_, errors) {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(
|
||||
__("Failed to create access source"),
|
||||
errors as GraphQLError[],
|
||||
),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Access source created successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
clearConnectorQueryParam();
|
||||
reset();
|
||||
ref.current?.close();
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(
|
||||
__("Failed to create access source"),
|
||||
error as GraphQLError,
|
||||
),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ref={ref}
|
||||
trigger={children}
|
||||
title={(
|
||||
<Breadcrumb
|
||||
items={[
|
||||
__("Access Reviews"),
|
||||
__("New Access Source"),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent padded className="space-y-4">
|
||||
<Field
|
||||
label={__("Name")}
|
||||
{...register("name")}
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
<Field label={__("Source type")}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="sourceType"
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<Option value="CSV">{__("CSV Upload")}</Option>
|
||||
<Option value="OAUTH2">{__("OAuth2 Connector")}</Option>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
{sourceType === "OAUTH2" && (
|
||||
<>
|
||||
<Field label={__("Provider")}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setValue("connectorId", undefined);
|
||||
}}
|
||||
>
|
||||
<Option value="GOOGLE_WORKSPACE">{__("Google Workspace")}</Option>
|
||||
<Option value="LINEAR">{__("Linear")}</Option>
|
||||
<Option value="SLACK">{__("Slack")}</Option>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={__("Connector")}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="connectorId"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
{selectableConnectors.map(connector => (
|
||||
<Option key={connector.id} value={connector.id}>
|
||||
{`${providerLabel(connector.provider)} (${new Date(connector.createdAt).toLocaleDateString(undefined, { month: "short", year: "numeric" })})`}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-txt-secondary text-sm">
|
||||
{__("Need a new connection? Connect your provider and come back to continue creating this source.")}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={startOAuthConnection}
|
||||
disabled={!provider}
|
||||
>
|
||||
{__("Connect")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{sourceType === "CSV" && (
|
||||
<>
|
||||
<Field
|
||||
label={__("CSV Data")}
|
||||
{...register("csvData")}
|
||||
type="textarea"
|
||||
placeholder="email,full_name,role,job_title,is_admin,active,external_id"
|
||||
/>
|
||||
<p className="text-txt-secondary text-sm">
|
||||
{__("Paste CSV content with a header row. Supported columns: email, full_name, role, job_title, is_admin, active, external_id.")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button disabled={isCreating} type="submit">
|
||||
{__("Create")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
export const createAccessSourceMutation = graphql`
|
||||
mutation accessSourceMutationsCreateMutation(
|
||||
$input: CreateAccessSourceInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
createAccessSource(input: $input) {
|
||||
accessSourceEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
...AccessSourceRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -34,12 +34,12 @@ import type { AccessReviewSourcesTabFragment$key } from "#/__generated__/core/Ac
|
||||
import type { AccessReviewSourcesTabPaginationQuery } from "#/__generated__/core/AccessReviewSourcesTabPaginationQuery.graphql";
|
||||
import type { AccessReviewSourcesTabQuery } from "#/__generated__/core/AccessReviewSourcesTabQuery.graphql";
|
||||
import type { AddAccessSourceDialogConnectorProviderInfoFragment$key } from "#/__generated__/core/AddAccessSourceDialogConnectorProviderInfoFragment.graphql";
|
||||
import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql";
|
||||
import type { accessSourceMutationsCreateMutation } from "#/__generated__/core/accessSourceMutationsCreateMutation.graphql";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
import { AccessSourceRow } from "../_components/AccessSourceRow";
|
||||
import { AddAccessSourceDialog, addAccessSourceDialogConnectorProviderInfoFragment } from "../dialogs/AddAccessSourceDialog";
|
||||
import { createAccessSourceMutation } from "../dialogs/CreateAccessSourceDialog";
|
||||
import { createAccessSourceMutation } from "../dialogs/accessSourceMutations";
|
||||
|
||||
export const accessReviewSourcesTabQuery = graphql`
|
||||
query AccessReviewSourcesTabQuery($organizationId: ID!) {
|
||||
@@ -131,7 +131,7 @@ export default function AccessReviewSourcesTab({ queryRef }: Props) {
|
||||
);
|
||||
|
||||
const [createAccessSource, isCreatingSource]
|
||||
= useMutation<CreateAccessSourceDialogMutation>(
|
||||
= useMutation<accessSourceMutationsCreateMutation>(
|
||||
createAccessSourceMutation,
|
||||
);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
const fragment = graphql`
|
||||
fragment CompliancePageSlackSectionFragment on Organization {
|
||||
canConnectSlack: permission(action: "core:connector:initiate")
|
||||
slackOAuth2Scopes
|
||||
slackConnections(first: 100) {
|
||||
__id
|
||||
edges {
|
||||
@@ -144,7 +145,9 @@ export function CompliancePageSlackSection(props: { fragmentRef: CompliancePageS
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="secondary" asChild>
|
||||
<a href={getSlackConnectionUrl(organizationId)}>{__("Connect")}</a>
|
||||
<a href={getSlackConnectionUrl(organizationId, organization.slackOAuth2Scopes)}>
|
||||
{__("Connect")}
|
||||
</a>
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
@@ -153,13 +156,15 @@ export function CompliancePageSlackSection(props: { fragmentRef: CompliancePageS
|
||||
);
|
||||
}
|
||||
|
||||
function getSlackConnectionUrl(organizationId: string): string {
|
||||
function getSlackConnectionUrl(organizationId: string, scopes: readonly string[]): string {
|
||||
const baseUrl = import.meta.env.VITE_API_URL || window.location.origin;
|
||||
const url = new URL("/api/console/v1/connectors/initiate", baseUrl);
|
||||
url.searchParams.append("organization_id", organizationId);
|
||||
url.searchParams.append("provider", "SLACK");
|
||||
for (const scope of scopes) {
|
||||
url.searchParams.append("scope", scope);
|
||||
}
|
||||
const redirectUrl = `/organizations/${organizationId}/compliance-page`;
|
||||
url.searchParams.append("continue", redirectUrl);
|
||||
const finalUrl = url.toString();
|
||||
return finalUrl;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user