mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 07:27:43 +00:00
feat: completed migration of app connection
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 47 KiB |
@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2";
|
import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||||
|
|
||||||
enum Region {
|
enum Region {
|
||||||
US = "us",
|
US = "us",
|
||||||
@@ -80,10 +81,7 @@ export const RegionSelect = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shouldDisplay =
|
const shouldDisplay =
|
||||||
window.location.origin.includes("https://app.infisical.com") ||
|
isInfisicalCloud() || window.location.origin.includes("http://localhost:8080");
|
||||||
window.location.origin.includes("https://us.infisical.com") ||
|
|
||||||
window.location.origin.includes("https://eu.infisical.com") ||
|
|
||||||
window.location.origin.includes("http://localhost:8080");
|
|
||||||
|
|
||||||
// only display region select for cloud
|
// only display region select for cloud
|
||||||
if (!shouldDisplay) return null;
|
if (!shouldDisplay) return null;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const FormLabel = ({
|
|||||||
)}
|
)}
|
||||||
{tooltipText && (
|
{tooltipText && (
|
||||||
<Tooltip content={tooltipText} className={tooltipClassName}>
|
<Tooltip content={tooltipText} className={tooltipClassName}>
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="1x" className="ml-2" />
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Label.Root>
|
</Label.Root>
|
||||||
|
|||||||
@@ -43,7 +43,13 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
RoleByIDPage: setRoute(
|
RoleByIDPage: setRoute(
|
||||||
"/organization/roles/$roleId",
|
"/organization/roles/$roleId",
|
||||||
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId"
|
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId"
|
||||||
)
|
),
|
||||||
|
AppConnections: {
|
||||||
|
GithubOauthCallbackPage: setRoute(
|
||||||
|
"/organization/app-connections/github/oauth/callback",
|
||||||
|
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback"
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
SecretManager: {
|
SecretManager: {
|
||||||
ApprovalPage: setRoute(
|
ApprovalPage: setRoute(
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export enum OrgPermissionSubjects {
|
|||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
AdminConsole = "organization-admin-console",
|
AdminConsole = "organization-admin-console",
|
||||||
AuditLogs = "audit-logs",
|
AuditLogs = "audit-logs",
|
||||||
ProjectTemplates = "project-templates"
|
ProjectTemplates = "project-templates",
|
||||||
|
AppConnections = "app-connections"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OrgPermissionAdminConsoleAction {
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
@@ -47,6 +48,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates];
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.AppConnections];
|
||||||
|
|
||||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import {
|
||||||
|
AwsConnectionMethod,
|
||||||
|
GitHubConnectionMethod,
|
||||||
|
TAppConnection
|
||||||
|
} from "@app/hooks/api/appConnections/types";
|
||||||
|
|
||||||
|
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
|
||||||
|
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
|
||||||
|
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" }
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||||
|
switch (method) {
|
||||||
|
case GitHubConnectionMethod.App:
|
||||||
|
return { name: "GitHub App", icon: faGithub };
|
||||||
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
return { name: "OAuth", icon: faPassport };
|
||||||
|
case AwsConnectionMethod.AccessKey:
|
||||||
|
return { name: "Access Key", icon: faKey };
|
||||||
|
case AwsConnectionMethod.AssumeRole:
|
||||||
|
return { name: "Assume Role", icon: faUser };
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const isInfisicalCloud = () =>
|
||||||
|
window.location.origin.includes("https://app.infisical.com") ||
|
||||||
|
window.location.origin.includes("https://us.infisical.com") ||
|
||||||
|
window.location.origin.includes("https://eu.infisical.com");
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum AppConnection {
|
||||||
|
AWS = "aws",
|
||||||
|
GitHub = "github"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./mutations";
|
||||||
|
export * from "./queries";
|
||||||
|
export * from "./types";
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import { appConnectionKeys } from "@app/hooks/api/appConnections/queries";
|
||||||
|
import {
|
||||||
|
TAppConnectionResponse,
|
||||||
|
TCreateAppConnectionDTO,
|
||||||
|
TDeleteAppConnectionDTO,
|
||||||
|
TUpdateAppConnectionDTO
|
||||||
|
} from "@app/hooks/api/appConnections/types";
|
||||||
|
|
||||||
|
export const useCreateAppConnection = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ app, ...params }: TCreateAppConnectionDTO) => {
|
||||||
|
const { data } = await apiRequest.post<TAppConnectionResponse>(
|
||||||
|
`/api/v1/app-connections/${app}`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.appConnection;
|
||||||
|
},
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() })
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateAppConnection = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ connectionId, app, ...params }: TUpdateAppConnectionDTO) => {
|
||||||
|
const { data } = await apiRequest.patch<TAppConnectionResponse>(
|
||||||
|
`/api/v1/app-connections/${app}/${connectionId}`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.appConnection;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { connectionId, app }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteAppConnection = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ connectionId, app }: TDeleteAppConnectionDTO) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/app-connections/${app}/${connectionId}`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { connectionId, app }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import {
|
||||||
|
TAppConnection,
|
||||||
|
TAppConnectionMap,
|
||||||
|
TAppConnectionOptions,
|
||||||
|
TGetAppConnection,
|
||||||
|
TListAppConnections
|
||||||
|
} from "@app/hooks/api/appConnections/types";
|
||||||
|
import {
|
||||||
|
TAppConnectionOption,
|
||||||
|
TAppConnectionOptionMap
|
||||||
|
} from "@app/hooks/api/appConnections/types/app-options";
|
||||||
|
|
||||||
|
export const appConnectionKeys = {
|
||||||
|
all: ["app-connection"] as const,
|
||||||
|
options: () => [...appConnectionKeys.all, "options"] as const,
|
||||||
|
list: () => [...appConnectionKeys.all, "list"] as const,
|
||||||
|
listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app],
|
||||||
|
byId: (app: AppConnection, templateId: string) =>
|
||||||
|
[...appConnectionKeys.all, app, "by-id", templateId] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAppConnectionOptions = (
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TAppConnectionOption[],
|
||||||
|
unknown,
|
||||||
|
TAppConnectionOption[],
|
||||||
|
ReturnType<typeof appConnectionKeys.options>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: appConnectionKeys.options(),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TAppConnectionOptions>(
|
||||||
|
"/api/v1/app-connections/options"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.appConnectionOptions;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetAppConnectionOption = <T extends AppConnection>(app: T) => {
|
||||||
|
const { data: options = [], isLoading } = useAppConnectionOptions();
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
option: (options.find((opt) => opt.app === app) as TAppConnectionOptionMap[T]) ?? {},
|
||||||
|
isLoading
|
||||||
|
}),
|
||||||
|
[options, app]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListAppConnections = (
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TAppConnection[],
|
||||||
|
unknown,
|
||||||
|
TAppConnection[],
|
||||||
|
ReturnType<typeof appConnectionKeys.list>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: appConnectionKeys.list(),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } =
|
||||||
|
await apiRequest.get<TListAppConnections<TAppConnection>>("/api/v1/app-connections");
|
||||||
|
|
||||||
|
return data.appConnections;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListAppConnectionsByApp = <T extends AppConnection>(
|
||||||
|
app: T,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TAppConnectionMap[T][],
|
||||||
|
unknown,
|
||||||
|
TAppConnectionMap[T][],
|
||||||
|
ReturnType<typeof appConnectionKeys.listByApp>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: appConnectionKeys.listByApp(app),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TListAppConnections<TAppConnectionMap[T]>>(
|
||||||
|
`/api/v1/app-connections/${app}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.appConnections;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetAppConnectionById = <T extends AppConnection>(
|
||||||
|
app: T,
|
||||||
|
connectionId: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TAppConnectionMap[T],
|
||||||
|
unknown,
|
||||||
|
TAppConnectionMap[T],
|
||||||
|
ReturnType<typeof appConnectionKeys.byId>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: appConnectionKeys.byId(app, connectionId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TGetAppConnection<TAppConnectionMap[T]>>(
|
||||||
|
`/api/v1/app-connections/${app}/${connectionId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.appConnection;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
|
||||||
|
export type TAppConnectionOptionBase = {
|
||||||
|
name: string;
|
||||||
|
methods: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAwsConnectionOption = TAppConnectionOptionBase & {
|
||||||
|
app: AppConnection.AWS;
|
||||||
|
accessKeyId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGitHubConnectionOption = TAppConnectionOptionBase & {
|
||||||
|
app: AppConnection.GitHub;
|
||||||
|
oauthClientId?: string;
|
||||||
|
appClientSlug?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption;
|
||||||
|
|
||||||
|
export type TAppConnectionOptionMap = {
|
||||||
|
[AppConnection.AWS]: TAwsConnectionOption;
|
||||||
|
[AppConnection.GitHub]: TGitHubConnectionOption;
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||||
|
|
||||||
|
export enum AwsConnectionMethod {
|
||||||
|
AssumeRole = "assume-role",
|
||||||
|
AccessKey = "access-key"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TAwsConnection = TRootAppConnection & { app: AppConnection.AWS } & (
|
||||||
|
| {
|
||||||
|
method: AwsConnectionMethod.AccessKey;
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: AwsConnectionMethod.AssumeRole;
|
||||||
|
credentials: {
|
||||||
|
roleArn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||||
|
|
||||||
|
export enum GitHubConnectionMethod {
|
||||||
|
App = "github-app",
|
||||||
|
OAuth = "oauth"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub } & (
|
||||||
|
| {
|
||||||
|
method: GitHubConnectionMethod.OAuth;
|
||||||
|
credentials: {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: GitHubConnectionMethod.App;
|
||||||
|
credentials: {
|
||||||
|
code: string;
|
||||||
|
installationId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-options";
|
||||||
|
import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection";
|
||||||
|
import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
|
||||||
|
|
||||||
|
export * from "./aws-connection";
|
||||||
|
export * from "./github-connection";
|
||||||
|
|
||||||
|
export type TAppConnection = TAwsConnection | TGitHubConnection;
|
||||||
|
|
||||||
|
export type TListAppConnections<T extends TAppConnection> = { appConnections: T[] };
|
||||||
|
export type TGetAppConnection<T extends TAppConnection> = { appConnection: T };
|
||||||
|
export type TAppConnectionOptions = { appConnectionOptions: TAppConnectionOption[] };
|
||||||
|
export type TAppConnectionResponse = { appConnection: TAppConnection };
|
||||||
|
|
||||||
|
export type TCreateAppConnectionDTO = Pick<
|
||||||
|
TAppConnection,
|
||||||
|
"name" | "credentials" | "method" | "app" | "description"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TUpdateAppConnectionDTO = Partial<
|
||||||
|
Pick<TAppConnection, "name" | "credentials" | "description">
|
||||||
|
> & {
|
||||||
|
connectionId: string;
|
||||||
|
app: AppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteAppConnectionDTO = {
|
||||||
|
app: AppConnection;
|
||||||
|
connectionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAppConnectionMap = {
|
||||||
|
[AppConnection.AWS]: TAwsConnection;
|
||||||
|
[AppConnection.GitHub]: TGitHubConnection;
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export type TRootAppConnection = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
version: number;
|
||||||
|
orgId: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
@@ -45,4 +45,5 @@ export type SubscriptionPlan = {
|
|||||||
pkiEst: boolean;
|
pkiEst: boolean;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
projectTemplates: boolean;
|
projectTemplates: boolean;
|
||||||
|
appConnections: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ContentLoader } from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import {
|
||||||
|
GitHubConnectionMethod,
|
||||||
|
TGitHubConnection,
|
||||||
|
useCreateAppConnection,
|
||||||
|
useUpdateAppConnection
|
||||||
|
} from "@app/hooks/api/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
|
||||||
|
type FormData = Pick<TGitHubConnection, "name" | "method" | "description"> & {
|
||||||
|
returnUrl?: string;
|
||||||
|
connectionId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GitHubOAuthCallbackPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const search = useSearch({
|
||||||
|
from: ROUTE_PATHS.Organization.AppConnections.GithubOauthCallbackPage.id
|
||||||
|
});
|
||||||
|
const updateAppConnection = useUpdateAppConnection();
|
||||||
|
const createAppConnection = useCreateAppConnection();
|
||||||
|
|
||||||
|
const { code, state, installation_id: installationId } = search;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
let formData: FormData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
formData = JSON.parse(localStorage.getItem("githubConnectionFormData") ?? "{}") as FormData;
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Invalid form state, redirecting..."
|
||||||
|
});
|
||||||
|
navigate({ to: "/" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate state
|
||||||
|
if (state !== localStorage.getItem("latestCSRFToken")) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Invalid state, redirecting..."
|
||||||
|
});
|
||||||
|
navigate({ to: "/" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem("githubConnectionFormData");
|
||||||
|
localStorage.removeItem("latestCSRFToken");
|
||||||
|
|
||||||
|
const { connectionId, name, description, returnUrl } = formData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (connectionId) {
|
||||||
|
await updateAppConnection.mutateAsync({
|
||||||
|
app: AppConnection.GitHub,
|
||||||
|
...(installationId
|
||||||
|
? {
|
||||||
|
connectionId,
|
||||||
|
credentials: {
|
||||||
|
code: code as string,
|
||||||
|
installationId: installationId as string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
connectionId,
|
||||||
|
credentials: {
|
||||||
|
code: code as string
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await createAppConnection.mutateAsync({
|
||||||
|
app: AppConnection.GitHub,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
...(installationId
|
||||||
|
? {
|
||||||
|
method: GitHubConnectionMethod.App,
|
||||||
|
credentials: {
|
||||||
|
code: code as string,
|
||||||
|
installationId: installationId as string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
method: GitHubConnectionMethod.OAuth,
|
||||||
|
credentials: {
|
||||||
|
code: code as string
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
createNotification({
|
||||||
|
title: `Failed to ${connectionId ? "update" : "add"} GitHub Connection`,
|
||||||
|
text: e.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
navigate({
|
||||||
|
to: returnUrl ?? "/organization/settings?selectedTab=app-connections"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully ${connectionId ? "updated" : "added"} GitHub Connection`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: returnUrl ?? "/organization/settings?selectedTab=app-connections"
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<ContentLoader text="Please wait! Authentication in process." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GitHubOAuthCallbackPage } from "./GithubOauthCallbackPage";
|
||||||
|
|
||||||
|
const GitHubOAuthCallbackPageQueryParamsSchema = z.object({
|
||||||
|
code: z.string(),
|
||||||
|
state: z.string(),
|
||||||
|
installation_id: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback"
|
||||||
|
)({
|
||||||
|
component: GitHubOAuthCallbackPage,
|
||||||
|
validateSearch: zodValidator(GitHubOAuthCallbackPageQueryParamsSchema)
|
||||||
|
});
|
||||||
+2
-1
@@ -49,7 +49,8 @@ export const formSchema = z.object({
|
|||||||
identity: generalPermissionSchema,
|
identity: generalPermissionSchema,
|
||||||
"organization-admin-console": adminConsolePermissionSchmea,
|
"organization-admin-console": adminConsolePermissionSchmea,
|
||||||
[OrgPermissionSubjects.Kms]: generalPermissionSchema,
|
[OrgPermissionSubjects.Kms]: generalPermissionSchema,
|
||||||
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema
|
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
|
||||||
|
[OrgPermissionSubjects.AppConnections]: generalPermissionSchema
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-1
@@ -69,7 +69,8 @@ const SIMPLE_PERMISSION_OPTIONS = [
|
|||||||
title: "External KMS",
|
title: "External KMS",
|
||||||
formName: OrgPermissionSubjects.Kms
|
formName: OrgPermissionSubjects.Kms
|
||||||
},
|
},
|
||||||
{ title: "Project Templates", formName: OrgPermissionSubjects.ProjectTemplates }
|
{ title: "Project Templates", formName: OrgPermissionSubjects.ProjectTemplates },
|
||||||
|
{ title: "App Connections", formName: OrgPermissionSubjects.AppConnections }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const SettingsPage = () => {
|
|||||||
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
|
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white">
|
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white">
|
||||||
<div className="w-full max-w-4xl px-6">
|
<div className="w-full max-w-7xl px-6">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
|
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
|
faPlus,
|
||||||
|
faWrench
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button } from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||||
|
import { withPermission } from "@app/hoc";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
|
||||||
|
import { AddAppConnectionModal, AppConnectionsTable } from "./components";
|
||||||
|
|
||||||
|
export const AppConnectionsTab = withPermission(
|
||||||
|
() => {
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addConnection"] as const);
|
||||||
|
|
||||||
|
// TODO: remove once live
|
||||||
|
if (!subscription?.appConnections)
|
||||||
|
return (
|
||||||
|
<div className="m-auto mt-40 flex w-full max-w-2xl flex-col items-center rounded-md bg-mineshaft-800 px-2 pt-4 text-bunker-300">
|
||||||
|
<FontAwesomeIcon icon={faWrench} size="2xl" />
|
||||||
|
<div className="flex flex-col items-center py-4">
|
||||||
|
<div className="text-lg text-mineshaft-200">
|
||||||
|
App Connections are currently unavailable.
|
||||||
|
</div>
|
||||||
|
<span className="text-mineshaft-300">Check back soon.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start gap-1">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">App Connections</p>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/integrations/app-connections/overview"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
<span>Docs</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-bunker-300">
|
||||||
|
Create and configure connections with third-party apps for re-use across Infisical
|
||||||
|
projects
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Create}
|
||||||
|
a={OrgPermissionSubjects.AppConnections}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("addConnection");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
Add Connection
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<AppConnectionsTable />
|
||||||
|
<AddAppConnectionModal
|
||||||
|
isOpen={popUp.addConnection.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("addConnection", isOpen)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: OrgPermissionActions.Read,
|
||||||
|
subject: OrgPermissionSubjects.AppConnections
|
||||||
|
}
|
||||||
|
);
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { TAppConnection } from "@app/hooks/api/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
|
||||||
|
import { AppConnectionForm } from "./AppConnectionForm";
|
||||||
|
import { AppConnectionsSelect } from "./AppConnectionList";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContentProps = {
|
||||||
|
onComplete: (appConnection: TAppConnection) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Content = ({ onComplete }: ContentProps) => {
|
||||||
|
const [selectedApp, setSelectedApp] = useState<AppConnection | null>(null);
|
||||||
|
|
||||||
|
if (selectedApp) {
|
||||||
|
return (
|
||||||
|
<AppConnectionForm
|
||||||
|
onComplete={onComplete}
|
||||||
|
onBack={() => setSelectedApp(null)}
|
||||||
|
app={selectedApp}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AppConnectionsSelect onSelect={setSelectedApp} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddAppConnectionModal = ({ isOpen, onOpenChange }: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-2xl"
|
||||||
|
title="Add Connection"
|
||||||
|
subTitle="Select a third-party app to connect to."
|
||||||
|
>
|
||||||
|
<Content onComplete={() => onOpenChange(false)} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||||
|
import {
|
||||||
|
TAppConnection,
|
||||||
|
useCreateAppConnection,
|
||||||
|
useUpdateAppConnection
|
||||||
|
} from "@app/hooks/api/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import { DiscriminativePick } from "@app/types";
|
||||||
|
|
||||||
|
import { AppConnectionHeader } from "../AppConnectionHeader";
|
||||||
|
import { AwsConnectionForm } from "./AwsConnectionForm";
|
||||||
|
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
||||||
|
|
||||||
|
type FormProps = {
|
||||||
|
onComplete: (appConnection: TAppConnection) => void;
|
||||||
|
} & ({ appConnection: TAppConnection } | { app: AppConnection });
|
||||||
|
|
||||||
|
type CreateFormProps = FormProps & { app: AppConnection };
|
||||||
|
type UpdateFormProps = FormProps & {
|
||||||
|
appConnection: TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||||
|
const createAppConnection = useCreateAppConnection();
|
||||||
|
const { name: appName } = APP_CONNECTION_MAP[app];
|
||||||
|
|
||||||
|
const onSubmit = async (
|
||||||
|
formData: DiscriminativePick<TAppConnection, "method" | "name" | "app" | "credentials">
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const connection = await createAppConnection.mutateAsync(formData);
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully added ${appName} Connection`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
onComplete(connection);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
title: `Failed to add ${appName} Connection`,
|
||||||
|
text: err.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (app) {
|
||||||
|
case AppConnection.AWS:
|
||||||
|
return <AwsConnectionForm onSubmit={onSubmit} />;
|
||||||
|
case AppConnection.GitHub:
|
||||||
|
return <GitHubConnectionForm />;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled App ${app}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||||
|
const updateAppConnection = useUpdateAppConnection();
|
||||||
|
const { name: appName } = APP_CONNECTION_MAP[appConnection.app];
|
||||||
|
|
||||||
|
const onSubmit = async (
|
||||||
|
formData: DiscriminativePick<TAppConnection, "method" | "name" | "app" | "credentials">
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const connection = await updateAppConnection.mutateAsync({
|
||||||
|
connectionId: appConnection.id,
|
||||||
|
...formData
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully updated ${appName} Connection`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
onComplete(connection);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
title: `Failed to update ${appName} Connection`,
|
||||||
|
text: err.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (appConnection.app) {
|
||||||
|
case AppConnection.AWS:
|
||||||
|
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||||
|
case AppConnection.GitHub:
|
||||||
|
return <GitHubConnectionForm appConnection={appConnection} />;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = { onBack?: () => void } & Pick<FormProps, "onComplete"> &
|
||||||
|
(
|
||||||
|
| { app: AppConnection; appConnection?: undefined }
|
||||||
|
| { app?: undefined; appConnection: TAppConnection }
|
||||||
|
);
|
||||||
|
export const AppConnectionForm = ({ onBack, ...props }: Props) => {
|
||||||
|
const { app, appConnection } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AppConnectionHeader
|
||||||
|
isConnected={Boolean(appConnection)}
|
||||||
|
app={appConnection?.app ?? app!}
|
||||||
|
onBack={onBack}
|
||||||
|
/>
|
||||||
|
{appConnection ? (
|
||||||
|
<UpdateForm {...props} appConnection={appConnection} />
|
||||||
|
) : (
|
||||||
|
<CreateForm {...props} app={app} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
ModalClose,
|
||||||
|
SecretInput,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
import { AwsConnectionMethod, TAwsConnection } from "@app/hooks/api/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
|
||||||
|
import {
|
||||||
|
genericAppConnectionFieldsSchema,
|
||||||
|
GenericAppConnectionsFields
|
||||||
|
} from "./GenericAppConnectionFields";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appConnection?: TAwsConnection;
|
||||||
|
onSubmit: (formData: FormData) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
|
app: z.literal(AppConnection.AWS)
|
||||||
|
});
|
||||||
|
|
||||||
|
const formSchema = z.discriminatedUnion("method", [
|
||||||
|
rootSchema.extend({
|
||||||
|
method: z.literal(AwsConnectionMethod.AssumeRole),
|
||||||
|
credentials: z.object({
|
||||||
|
roleArn: z.string().trim().min(1, "Role ARN required")
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
rootSchema.extend({
|
||||||
|
method: z.literal(AwsConnectionMethod.AccessKey),
|
||||||
|
credentials: z.object({
|
||||||
|
accessKeyId: z.string().trim().min(1, "Access Key ID required"),
|
||||||
|
secretAccessKey: z.string().trim().min(1, "Secret Access Key required")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const AwsConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||||
|
const isUpdate = Boolean(appConnection);
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: appConnection ?? {
|
||||||
|
app: AppConnection.AWS,
|
||||||
|
method: AwsConnectionMethod.AssumeRole
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
const selectedMethod = watch("method");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
|
<Controller
|
||||||
|
name="method"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText={`The method you would like to use to connect with ${
|
||||||
|
APP_CONNECTION_MAP[AppConnection.AWS].name
|
||||||
|
}. This field cannot be changed after creation.`}
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Method"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
isDisabled={isUpdate}
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
>
|
||||||
|
{Object.values(AwsConnectionMethod).map((method) => {
|
||||||
|
return (
|
||||||
|
<SelectItem value={method} key={method}>
|
||||||
|
{getAppConnectionMethodDetails(method).name}{" "}
|
||||||
|
{method === AwsConnectionMethod.AssumeRole ? " (Recommended)" : ""}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedMethod === AwsConnectionMethod.AssumeRole ? (
|
||||||
|
<Controller
|
||||||
|
name="credentials.roleArn"
|
||||||
|
control={control}
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Role ARN"
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<SecretInput
|
||||||
|
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="credentials.accessKeyId"
|
||||||
|
control={control}
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Access Key ID"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={"*".repeat(20)}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="credentials.secretAccessKey"
|
||||||
|
control={control}
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Secret Access Key"
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<SecretInput
|
||||||
|
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Update Credentials" : "Connect to AWS"}
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { FormControl, Input, TextArea } from "@app/components/v2";
|
||||||
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
|
export const genericAppConnectionFieldsSchema = z.object({
|
||||||
|
name: slugSchema({ min: 1, max: 32, field: "Name" }),
|
||||||
|
description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericAppConnectionsFields = () => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors }
|
||||||
|
} = useFormContext<{ name: string; description?: string | null }>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormControl
|
||||||
|
helperText="Name must be slug-friendly"
|
||||||
|
errorText={errors.name?.message}
|
||||||
|
isError={Boolean(errors.name?.message)}
|
||||||
|
label="Name"
|
||||||
|
>
|
||||||
|
<Input autoFocus placeholder="my-app-connection" {...register("name")} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
errorText={errors.description?.message}
|
||||||
|
isError={Boolean(errors.description?.message)}
|
||||||
|
label="Description"
|
||||||
|
isOptional
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
className="h-20 !resize-none"
|
||||||
|
placeholder="Connection description..."
|
||||||
|
{...register("description")}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||||
|
import {
|
||||||
|
GitHubConnectionMethod,
|
||||||
|
TGitHubConnection,
|
||||||
|
useGetAppConnectionOption
|
||||||
|
} from "@app/hooks/api/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
|
||||||
|
import {
|
||||||
|
genericAppConnectionFieldsSchema,
|
||||||
|
GenericAppConnectionsFields
|
||||||
|
} from "./GenericAppConnectionFields";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appConnection?: TGitHubConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
|
app: z.literal(AppConnection.GitHub),
|
||||||
|
method: z.nativeEnum(GitHubConnectionMethod)
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
||||||
|
const isUpdate = Boolean(appConnection);
|
||||||
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
option: { oauthClientId, appClientSlug },
|
||||||
|
isLoading
|
||||||
|
} = useGetAppConnectionOption(AppConnection.GitHub);
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: appConnection ?? {
|
||||||
|
app: AppConnection.GitHub,
|
||||||
|
method: GitHubConnectionMethod.App
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
const selectedMethod = watch("method");
|
||||||
|
|
||||||
|
const onSubmit = (formData: FormData) => {
|
||||||
|
setIsRedirecting(true);
|
||||||
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
|
localStorage.setItem("latestCSRFToken", state);
|
||||||
|
localStorage.setItem(
|
||||||
|
"githubConnectionFormData",
|
||||||
|
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (formData.method) {
|
||||||
|
case GitHubConnectionMethod.App:
|
||||||
|
window.location.assign(
|
||||||
|
`https://github.com/apps/${appClientSlug}/installations/new?state=${state}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
window.location.assign(
|
||||||
|
`https://github.com/login/oauth/authorize?client_id=${oauthClientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/app-connections/github/oauth/callback&state=${state}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled GitHub Connection method: ${(formData as FormData).method}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let isMissingConfig: boolean;
|
||||||
|
|
||||||
|
switch (selectedMethod) {
|
||||||
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
isMissingConfig = !oauthClientId;
|
||||||
|
break;
|
||||||
|
case GitHubConnectionMethod.App:
|
||||||
|
isMissingConfig = !appClientSlug;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled GitHub Connection method: ${selectedMethod}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
|
<Controller
|
||||||
|
name="method"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText={`The method you would like to use to connect with ${
|
||||||
|
APP_CONNECTION_MAP[AppConnection.GitHub].name
|
||||||
|
}. This field cannot be changed after creation.`}
|
||||||
|
errorText={
|
||||||
|
!isLoading && isMissingConfig
|
||||||
|
? `Environment variables have not been configured. ${
|
||||||
|
isInfisicalCloud()
|
||||||
|
? "Please contact Infisical."
|
||||||
|
: `See Docs to configure GitHub ${methodDetails.name} Connections.`
|
||||||
|
}`
|
||||||
|
: error?.message
|
||||||
|
}
|
||||||
|
isError={Boolean(error?.message) || isMissingConfig}
|
||||||
|
label="Method"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
isDisabled={isUpdate}
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
>
|
||||||
|
{Object.values(GitHubConnectionMethod).map((method) => {
|
||||||
|
return (
|
||||||
|
<SelectItem value={method} key={method}>
|
||||||
|
{methodDetails.name}{" "}
|
||||||
|
{method === GitHubConnectionMethod.App ? " (Recommended)" : ""}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isSubmitting || isRedirecting}
|
||||||
|
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Reconnect to GitHub" : "Connect to GitHub"}
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./AppConnectionForm";
|
||||||
|
export * from "./GenericAppConnectionFields";
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
app: AppConnection;
|
||||||
|
isConnected: boolean;
|
||||||
|
onBack?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppConnectionHeader = ({ app, isConnected, onBack }: Props) => {
|
||||||
|
const appDetails = APP_CONNECTION_MAP[app];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4 flex w-full items-start gap-2 border-b border-mineshaft-500 pb-4">
|
||||||
|
<img
|
||||||
|
alt={`${appDetails.name} logo`}
|
||||||
|
src={`/images/integrations/${appDetails.image}`}
|
||||||
|
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center text-mineshaft-300">
|
||||||
|
{appDetails.name}
|
||||||
|
<a
|
||||||
|
href={`https://infisical.com/docs/integrations/app-connections/${app}`}
|
||||||
|
target="_blank"
|
||||||
|
className="mb-1 ml-1"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
|
||||||
|
<span>Docs</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm leading-4 text-mineshaft-400">
|
||||||
|
{isConnected ? `${appDetails.name} Connection` : `Connect to ${appDetails.name}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{onBack && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-auto mt-1 text-xs text-mineshaft-400 underline underline-offset-2 hover:text-mineshaft-300"
|
||||||
|
onClick={onBack}
|
||||||
|
>
|
||||||
|
Select another App
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Spinner, Tooltip } from "@app/components/v2";
|
||||||
|
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||||
|
import { useAppConnectionOptions } from "@app/hooks/api/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSelect: (app: AppConnection) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppConnectionsSelect = ({ onSelect }: Props) => {
|
||||||
|
const { isLoading, data: appConnectionOptions } = useAppConnectionOptions();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||||
|
<Spinner size="lg" className="text-mineshaft-500" />
|
||||||
|
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-5 gap-2">
|
||||||
|
{appConnectionOptions?.map((option) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={option.app}
|
||||||
|
onClick={() => onSelect(option.app)}
|
||||||
|
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`/images/integrations/${APP_CONNECTION_MAP[option.app].image}`}
|
||||||
|
height={50}
|
||||||
|
width={50}
|
||||||
|
className="mt-auto"
|
||||||
|
alt={`${APP_CONNECTION_MAP[option.app].name} logo`}
|
||||||
|
/>
|
||||||
|
<div className="mt-auto max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
|
{APP_CONNECTION_MAP[option.app].name}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<Tooltip
|
||||||
|
side="bottom"
|
||||||
|
className="max-w-sm py-4"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p className="mb-2">Infisical is constantly adding support for more connections.</p>
|
||||||
|
<p>
|
||||||
|
{`If you don't see the third-party
|
||||||
|
app you're looking for,`}{" "}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
className="underline hover:text-mineshaft-300"
|
||||||
|
href="https://infisical.com/slack"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
let us know on Slack
|
||||||
|
</a>{" "}
|
||||||
|
or{" "}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
className="underline hover:text-mineshaft-300"
|
||||||
|
href="https://github.com/Infisical/infisical/discussions"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
make a request on GitHub
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<FontAwesomeIcon className="mt-auto text-xl" icon={faWrench} />
|
||||||
|
<div className="mt-auto max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
|
Coming Soon
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import {
|
||||||
|
faAsterisk,
|
||||||
|
faCheck,
|
||||||
|
faCopy,
|
||||||
|
faEdit,
|
||||||
|
faEllipsisV,
|
||||||
|
faInfoCircle,
|
||||||
|
faTrash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
IconButton,
|
||||||
|
Td,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { TAppConnection } from "@app/hooks/api/appConnections";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appConnection: TAppConnection;
|
||||||
|
onDelete: (appConnection: TAppConnection) => void;
|
||||||
|
onEditCredentials: (appConnection: TAppConnection) => void;
|
||||||
|
onEditDetails: (appConnection: TAppConnection) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppConnectionRow = ({
|
||||||
|
appConnection,
|
||||||
|
onDelete,
|
||||||
|
onEditCredentials,
|
||||||
|
onEditDetails
|
||||||
|
}: Props) => {
|
||||||
|
const { id, name, method, app, description } = appConnection;
|
||||||
|
|
||||||
|
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||||
|
|
||||||
|
const handleCopyId = useCallback(() => {
|
||||||
|
setIsIdCopied.on();
|
||||||
|
navigator.clipboard.writeText(id);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Connection ID copied to clipboard",
|
||||||
|
type: "info"
|
||||||
|
});
|
||||||
|
|
||||||
|
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isIdCopied]);
|
||||||
|
|
||||||
|
const methodDetails = getAppConnectionMethodDetails(method);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr
|
||||||
|
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700")}
|
||||||
|
key={`app-connection-${id}`}
|
||||||
|
>
|
||||||
|
<Td>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
alt={`${APP_CONNECTION_MAP[app].name} integration`}
|
||||||
|
src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`}
|
||||||
|
className="mr-0.5 h-5 w-5"
|
||||||
|
/>
|
||||||
|
<span className="hidden lg:inline">{APP_CONNECTION_MAP[app].name}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td className="!min-w-[8rem] max-w-0">
|
||||||
|
<div className="flex w-full items-center">
|
||||||
|
<p className="truncate">{name}</p>
|
||||||
|
{description && (
|
||||||
|
<Tooltip content={description}>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="ml-1 text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td className="!min-w-[8rem] max-w-0">
|
||||||
|
<p className="truncate">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
size="sm"
|
||||||
|
className="mr-1.5 text-mineshaft-300/75"
|
||||||
|
icon={methodDetails.icon}
|
||||||
|
/>
|
||||||
|
{methodDetails.name}
|
||||||
|
</p>
|
||||||
|
</Td>
|
||||||
|
|
||||||
|
<Td>
|
||||||
|
<Tooltip className="max-w-sm text-center" content="Options">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="Options"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="w-6"
|
||||||
|
variant="plain"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||||
|
onClick={() => handleCopyId()}
|
||||||
|
>
|
||||||
|
Copy Connection ID
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.AppConnections}
|
||||||
|
>
|
||||||
|
{(isAllowed: boolean) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
|
onClick={() => onEditDetails(appConnection)}
|
||||||
|
>
|
||||||
|
Edit Details
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.AppConnections}
|
||||||
|
>
|
||||||
|
{(isAllowed: boolean) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faAsterisk} />}
|
||||||
|
onClick={() => onEditCredentials(appConnection)}
|
||||||
|
>
|
||||||
|
Edit Credentials
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.AppConnections}
|
||||||
|
>
|
||||||
|
{(isAllowed: boolean) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
onClick={() => onDelete(appConnection)}
|
||||||
|
>
|
||||||
|
Delete Connection
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Tooltip>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
+321
@@ -0,0 +1,321 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faCheckCircle,
|
||||||
|
faFilter,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faPlug,
|
||||||
|
faSearch
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useSubscription } from "@app/context";
|
||||||
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
|
import { TAppConnection, useListAppConnections } from "@app/hooks/api/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
|
import { AppConnectionRow } from "./AppConnectionRow";
|
||||||
|
import { DeleteAppConnectionModal } from "./DeleteAppConnectionModal";
|
||||||
|
import { EditAppConnectionCredentialsModal } from "./EditAppConnectionCredentialsModal";
|
||||||
|
import { EditAppConnectionDetailsModal } from "./EditAppConnectionDetailsModal";
|
||||||
|
|
||||||
|
enum AppConnectionsOrderBy {
|
||||||
|
App = "app",
|
||||||
|
Name = "name",
|
||||||
|
Method = "method"
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppConnectionFilters = {
|
||||||
|
apps: AppConnection[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppConnectionsTable = () => {
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
const { isLoading, data: appConnections = [] } = useListAppConnections({
|
||||||
|
enabled: subscription?.appConnections
|
||||||
|
});
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
|
"deleteConnection",
|
||||||
|
"editCredentials",
|
||||||
|
"editDetails"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const [filters, setFilters] = useState<AppConnectionFilters>({
|
||||||
|
apps: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
setPage,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
setPerPage,
|
||||||
|
offset,
|
||||||
|
orderDirection,
|
||||||
|
toggleOrderDirection,
|
||||||
|
orderBy,
|
||||||
|
setOrderDirection,
|
||||||
|
setOrderBy
|
||||||
|
} = usePagination<AppConnectionsOrderBy>(AppConnectionsOrderBy.App, { initPerPage: 20 });
|
||||||
|
|
||||||
|
const filteredAppConnections = useMemo(
|
||||||
|
() =>
|
||||||
|
appConnections
|
||||||
|
.filter((appConnection) => {
|
||||||
|
const { app, method, name } = appConnection;
|
||||||
|
|
||||||
|
if (filters.apps.length && !filters.apps.includes(app)) return false;
|
||||||
|
|
||||||
|
const searchValue = search.trim().toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
APP_CONNECTION_MAP[app].name.toLowerCase().includes(searchValue) ||
|
||||||
|
getAppConnectionMethodDetails(method).name.toLowerCase().includes(searchValue) ||
|
||||||
|
name.toLowerCase().includes(searchValue)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const [connectionOne, connectionTwo] =
|
||||||
|
orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||||
|
|
||||||
|
switch (orderBy) {
|
||||||
|
case AppConnectionsOrderBy.Name:
|
||||||
|
return connectionOne.name
|
||||||
|
.toLowerCase()
|
||||||
|
.localeCompare(connectionTwo.name.toLowerCase());
|
||||||
|
case AppConnectionsOrderBy.Method:
|
||||||
|
return getAppConnectionMethodDetails(connectionOne.method)
|
||||||
|
.name.toLowerCase()
|
||||||
|
.localeCompare(
|
||||||
|
getAppConnectionMethodDetails(connectionTwo.method).name.toLowerCase()
|
||||||
|
);
|
||||||
|
case AppConnectionsOrderBy.App:
|
||||||
|
default:
|
||||||
|
return APP_CONNECTION_MAP[connectionOne.app].name
|
||||||
|
.toLowerCase()
|
||||||
|
.localeCompare(APP_CONNECTION_MAP[connectionTwo.app].name.toLowerCase());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[appConnections, orderDirection, search, orderBy, filters]
|
||||||
|
);
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount: filteredAppConnections.length,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSort = (column: AppConnectionsOrderBy) => {
|
||||||
|
if (column === orderBy) {
|
||||||
|
toggleOrderDirection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrderBy(column);
|
||||||
|
setOrderDirection(OrderByDirection.ASC);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClassName = (col: AppConnectionsOrderBy) =>
|
||||||
|
twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||||
|
|
||||||
|
const getColSortIcon = (col: AppConnectionsOrderBy) =>
|
||||||
|
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||||
|
|
||||||
|
const isTableFiltered = Boolean(filters.apps.length);
|
||||||
|
|
||||||
|
const handleDelete = (appConnection: TAppConnection) =>
|
||||||
|
handlePopUpOpen("deleteConnection", appConnection);
|
||||||
|
|
||||||
|
const handleEditCredentials = (appConnection: TAppConnection) =>
|
||||||
|
handlePopUpOpen("editCredentials", appConnection);
|
||||||
|
|
||||||
|
const handleEditDetails = (appConnection: TAppConnection) =>
|
||||||
|
handlePopUpOpen("editDetails", appConnection);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search integrations..."
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="Filter Connections"
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||||
|
isTableFiltered && "border-primary/50 text-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFilter} />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||||
|
<DropdownMenuLabel>Filter by Apps</DropdownMenuLabel>
|
||||||
|
{appConnections.length ? (
|
||||||
|
[...new Set(appConnections.map(({ app }) => app))].map((app) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
apps: prev.apps.includes(app)
|
||||||
|
? prev.apps.filter((a) => a !== app)
|
||||||
|
: [...prev.apps, app]
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
key={app}
|
||||||
|
icon={
|
||||||
|
filters.apps.includes(app) && (
|
||||||
|
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
alt={`${APP_CONNECTION_MAP[app].name} integration`}
|
||||||
|
src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span>{APP_CONNECTION_MAP[app].name}</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<DropdownMenuItem isDisabled>No Connections Configured</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<TableContainer className="mt-4">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="w-1/4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
App
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(AppConnectionsOrderBy.App)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(AppConnectionsOrderBy.App)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(AppConnectionsOrderBy.App)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th className="w-1/3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
Name
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(AppConnectionsOrderBy.Name)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(AppConnectionsOrderBy.Name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(AppConnectionsOrderBy.Name)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Method
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(AppConnectionsOrderBy.Method)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(AppConnectionsOrderBy.Method)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(AppConnectionsOrderBy.Method)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && (
|
||||||
|
<TableSkeleton innerKey="app-connections-table" columns={4} key="app-connections" />
|
||||||
|
)}
|
||||||
|
{filteredAppConnections.slice(offset, perPage * page).map((connection) => (
|
||||||
|
<AppConnectionRow
|
||||||
|
appConnection={connection}
|
||||||
|
key={connection.id}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onEditCredentials={handleEditCredentials}
|
||||||
|
onEditDetails={handleEditDetails}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{Boolean(filteredAppConnections.length) && (
|
||||||
|
<Pagination
|
||||||
|
count={filteredAppConnections.length}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={setPerPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLoading && !filteredAppConnections?.length && (
|
||||||
|
<EmptyState
|
||||||
|
title={
|
||||||
|
appConnections.length
|
||||||
|
? "No App Connections match search..."
|
||||||
|
: "No App Connections have been configured"
|
||||||
|
}
|
||||||
|
icon={appConnections.length ? faSearch : faPlug}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
<DeleteAppConnectionModal
|
||||||
|
isOpen={popUp.deleteConnection.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("deleteConnection", isOpen)}
|
||||||
|
appConnection={popUp.deleteConnection.data}
|
||||||
|
/>
|
||||||
|
<EditAppConnectionCredentialsModal
|
||||||
|
isOpen={popUp.editCredentials.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("editCredentials", isOpen)}
|
||||||
|
appConnection={popUp.editCredentials.data}
|
||||||
|
/>
|
||||||
|
<EditAppConnectionDetailsModal
|
||||||
|
isOpen={popUp.editDetails.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
|
||||||
|
appConnection={popUp.editDetails.data}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||||
|
import { TAppConnection, useDeleteAppConnection } from "@app/hooks/api/appConnections";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appConnection?: TAppConnection;
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeleteAppConnectionModal = ({ isOpen, onOpenChange, appConnection }: Props) => {
|
||||||
|
const deleteAppConnection = useDeleteAppConnection();
|
||||||
|
|
||||||
|
if (!appConnection) return null;
|
||||||
|
|
||||||
|
const { id: connectionId, name, app } = appConnection;
|
||||||
|
|
||||||
|
const handleDeleteAppConnection = async () => {
|
||||||
|
try {
|
||||||
|
await deleteAppConnection.mutateAsync({
|
||||||
|
connectionId,
|
||||||
|
app
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully removed ${APP_CONNECTION_MAP[app].name} connection`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Failed remove ${APP_CONNECTION_MAP[app].name} connection`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onChange={onOpenChange}
|
||||||
|
title={`Are you sure want to delete ${name}?`}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={handleDeleteAppConnection}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||||
|
import { TAppConnection } from "@app/hooks/api/appConnections";
|
||||||
|
|
||||||
|
import { AppConnectionForm } from "./AppConnectionForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
appConnection?: TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditAppConnectionCredentialsModal = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
appConnection
|
||||||
|
}: Props) => {
|
||||||
|
if (!appConnection) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-2xl"
|
||||||
|
title="Edit Connection Credentials"
|
||||||
|
subTitle={`Update the credentials for this ${
|
||||||
|
appConnection ? APP_CONNECTION_MAP[appConnection.app].name : "App"
|
||||||
|
} Connection.`}
|
||||||
|
>
|
||||||
|
<AppConnectionForm onComplete={() => onOpenChange(false)} appConnection={appConnection} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||||
|
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||||
|
import { TAppConnection, useUpdateAppConnection } from "@app/hooks/api/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import { DiscriminativePick } from "@app/types";
|
||||||
|
|
||||||
|
import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields } from "./AppConnectionForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
appConnection?: TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
|
app: z.nativeEnum(AppConnection)
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type ContentProps = { appConnection: TAppConnection; onComplete: () => void };
|
||||||
|
|
||||||
|
const Content = ({ appConnection, onComplete }: ContentProps) => {
|
||||||
|
const updateAppConnection = useUpdateAppConnection();
|
||||||
|
const { name: appName } = APP_CONNECTION_MAP[appConnection.app];
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: appConnection.name,
|
||||||
|
app: appConnection.app,
|
||||||
|
description: appConnection.description
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
const onSubmit = async (formData: DiscriminativePick<TAppConnection, "name" | "app">) => {
|
||||||
|
try {
|
||||||
|
await updateAppConnection.mutateAsync({
|
||||||
|
connectionId: appConnection.id,
|
||||||
|
...formData
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully updated ${appName} Connection`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
onComplete();
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
title: `Failed to update ${appName} Connection`,
|
||||||
|
text: err.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<GenericAppConnectionsFields />
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
>
|
||||||
|
Update Details
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditAppConnectionDetailsModal = ({ isOpen, onOpenChange, appConnection }: Props) => {
|
||||||
|
if (!appConnection) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-2xl"
|
||||||
|
title="Edit Connection Name"
|
||||||
|
subTitle={`Update the name for this ${
|
||||||
|
appConnection ? APP_CONNECTION_MAP[appConnection.app].name : "App"
|
||||||
|
} Connection.`}
|
||||||
|
>
|
||||||
|
<Content appConnection={appConnection} onComplete={() => onOpenChange(false)} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./AddAppConnectionModal";
|
||||||
|
export * from "./AppConnectionsTable";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { AppConnectionsTab } from "./AppConnectionsTab";
|
||||||
+44
-36
@@ -1,8 +1,9 @@
|
|||||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import { useOrgPermission } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
@@ -14,45 +15,52 @@ export const ImportTab = () => {
|
|||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const);
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<OrgPermissionCan
|
||||||
<div className="flex justify-between">
|
I={OrgPermissionActions.Create}
|
||||||
<div className="flex items-center gap-2">
|
a={OrgPermissionSubjects.Workspace}
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Import from external source</p>
|
renderGuardBanner
|
||||||
|
passThrough={false}
|
||||||
|
>
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Import from external source</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href="https://infisical.com/docs/documentation/guides/migrating-from-envkey"
|
href="https://infisical.com/docs/documentation/guides/migrating-from-envkey"
|
||||||
>
|
>
|
||||||
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
Docs
|
Docs
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faArrowUpRightFromSquare}
|
icon={faArrowUpRightFromSquare}
|
||||||
className="mb-[0.07rem] ml-1.5 text-xxs"
|
className="mb-[0.07rem] ml-1.5 text-xxs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("selectImportPlatform");
|
||||||
|
}}
|
||||||
|
isDisabled={membership?.role !== ProjectMembershipRole.Admin}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="mb-4 text-gray-400">Import data from another platform to Infisical.</p>
|
||||||
|
|
||||||
<Button
|
<SelectImportFromPlatformModal
|
||||||
onClick={() => {
|
isOpen={popUp.selectImportPlatform.isOpen}
|
||||||
handlePopUpOpen("selectImportPlatform");
|
onToggle={(state) => handlePopUpToggle("selectImportPlatform", state)}
|
||||||
}}
|
/>
|
||||||
isDisabled={membership?.role !== ProjectMembershipRole.Admin}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
>
|
|
||||||
Import
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-4 text-gray-400">Import data from another platform to Infisical.</p>
|
</OrgPermissionCan>
|
||||||
|
|
||||||
<SelectImportFromPlatformModal
|
|
||||||
isOpen={popUp.selectImportPlatform.isOpen}
|
|
||||||
onToggle={(state) => handlePopUpToggle("selectImportPlatform", state)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+20
-33
@@ -1,11 +1,10 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
|
||||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
|
||||||
|
|
||||||
|
import { AppConnectionsTab } from "../AppConnectionsTab";
|
||||||
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
||||||
import { ImportTab } from "../ImportTab";
|
import { ImportTab } from "../ImportTab";
|
||||||
import { OrgAuthTab } from "../OrgAuthTab";
|
import { OrgAuthTab } from "../OrgAuthTab";
|
||||||
@@ -14,19 +13,25 @@ import { OrgGeneralTab } from "../OrgGeneralTab";
|
|||||||
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
|
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
|
||||||
import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
|
import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{ name: "General", key: "tab-org-general" },
|
|
||||||
{ name: "Security", key: "tab-org-security" },
|
|
||||||
{ name: "Encryption", key: "tab-org-encryption" },
|
|
||||||
{ name: "Workflow Integrations", key: "workflow-integrations" },
|
|
||||||
{ name: "Audit Log Streams", key: "tag-audit-log-streams" },
|
|
||||||
{ name: "Import", key: "tab-import" },
|
|
||||||
{ name: "Project Templates", key: "project-templates" }
|
|
||||||
];
|
|
||||||
export const OrgTabGroup = () => {
|
export const OrgTabGroup = () => {
|
||||||
const search = useSearch({
|
const search = useSearch({
|
||||||
from: ROUTE_PATHS.Organization.SettingsPage.id
|
from: ROUTE_PATHS.Organization.SettingsPage.id
|
||||||
});
|
});
|
||||||
|
const tabs = [
|
||||||
|
{ name: "General", key: "tab-org-general", component: OrgGeneralTab },
|
||||||
|
{ name: "Security", key: "tab-org-security", component: OrgAuthTab },
|
||||||
|
{ name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab },
|
||||||
|
{
|
||||||
|
name: "Workflow Integrations",
|
||||||
|
key: "workflow-integrations",
|
||||||
|
component: OrgWorkflowIntegrationTab
|
||||||
|
},
|
||||||
|
{ name: "App Connections", key: "app-connections", component: AppConnectionsTab },
|
||||||
|
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
|
||||||
|
{ name: "Import", key: "tab-import", component: ImportTab },
|
||||||
|
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab }
|
||||||
|
];
|
||||||
|
|
||||||
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
|
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -38,29 +43,11 @@ export const OrgTabGroup = () => {
|
|||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={tabs[0].key}>
|
{tabs.map(({ key, component: Component }) => (
|
||||||
<OrgGeneralTab />
|
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||||
</TabPanel>
|
<Component />
|
||||||
<TabPanel value={tabs[1].key}>
|
|
||||||
<OrgAuthTab />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={tabs[2].key}>
|
|
||||||
<OrgEncryptionTab />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={tabs[3].key}>
|
|
||||||
<OrgWorkflowIntegrationTab />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={tabs[4].key}>
|
|
||||||
<AuditLogStreamsTab />
|
|
||||||
</TabPanel>
|
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Workspace}>
|
|
||||||
<TabPanel value={tabs[5].key}>
|
|
||||||
<ImportTab />
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</OrgPermissionCan>
|
))}
|
||||||
<TabPanel value={tabs[6].key}>
|
|
||||||
<ProjectTemplatesTab />
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
||||||
import { zodValidator } from "@tanstack/zod-adapter";
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -12,5 +12,8 @@ export const Route = createFileRoute(
|
|||||||
"/_authenticate/_inject-org-details/organization/_layout/settings"
|
"/_authenticate/_inject-org-details/organization/_layout/settings"
|
||||||
)({
|
)({
|
||||||
component: SettingsPage,
|
component: SettingsPage,
|
||||||
validateSearch: zodValidator(SettingsPageQueryParams)
|
validateSearch: zodValidator(SettingsPageQueryParams),
|
||||||
|
search: {
|
||||||
|
middlewares: [stripSearchParams({ selectedTab: "" })]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -34,11 +34,11 @@ import { useDebounce } from "@app/hooks";
|
|||||||
import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard";
|
import { useGetProjectSecretsQuickSearch } from "@app/hooks/api/dashboard";
|
||||||
import { WsTag } from "@app/hooks/api/tags/types";
|
import { WsTag } from "@app/hooks/api/tags/types";
|
||||||
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||||
|
import { RowType } from "@app/pages/secret-manager/SecretDashboardPage/SecretMainPage.types";
|
||||||
|
|
||||||
import { QuickSearchDynamicSecretItem } from "./QuickSearchDynamicSecretItem";
|
import { QuickSearchDynamicSecretItem } from "./QuickSearchDynamicSecretItem";
|
||||||
import { QuickSearchFolderItem } from "./QuickSearchFolderItem";
|
import { QuickSearchFolderItem } from "./QuickSearchFolderItem";
|
||||||
import { QuickSearchSecretItem } from "./QuickSearchSecretItem";
|
import { QuickSearchSecretItem } from "./QuickSearchSecretItem";
|
||||||
import { RowType } from "@app/pages/secret-manager/SecretDashboardPage/SecretMainPage.types";
|
|
||||||
|
|
||||||
export type QuickSearchModalProps = {
|
export type QuickSearchModalProps = {
|
||||||
environments: WorkspaceEnv[];
|
environments: WorkspaceEnv[];
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ import { Route as projectMemberDetailsByIDPageRouteCertManagerImport } from './p
|
|||||||
import { Route as projectIdentityDetailsByIDPageRouteCertManagerImport } from './pages/project/IdentityDetailsByIDPage/route-cert-manager'
|
import { Route as projectIdentityDetailsByIDPageRouteCertManagerImport } from './pages/project/IdentityDetailsByIDPage/route-cert-manager'
|
||||||
import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route'
|
import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route'
|
||||||
import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
|
import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
|
||||||
|
import { Route as organizationAppConnectionsGithubOauthCallbackPageRouteImport } from './pages/organization/AppConnections/GithubOauthCallbackPage/route'
|
||||||
|
|
||||||
// Create Virtual Routes
|
// Create Virtual Routes
|
||||||
|
|
||||||
@@ -614,6 +615,13 @@ const certManagerCertAuthDetailsByIDPageRouteRoute =
|
|||||||
getParentRoute: () => certManagerLayoutRoute,
|
getParentRoute: () => certManagerLayoutRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const organizationAppConnectionsGithubOauthCallbackPageRouteRoute =
|
||||||
|
organizationAppConnectionsGithubOauthCallbackPageRouteImport.update({
|
||||||
|
id: '/app-connections/github/oauth/callback',
|
||||||
|
path: '/app-connections/github/oauth/callback',
|
||||||
|
getParentRoute: () => organizationLayoutRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
// Populate the FileRoutesByPath interface
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -1150,6 +1158,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteSecretManagerImport
|
preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteSecretManagerImport
|
||||||
parentRoute: typeof secretManagerLayoutImport
|
parentRoute: typeof secretManagerLayoutImport
|
||||||
}
|
}
|
||||||
|
'/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback': {
|
||||||
|
id: '/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback'
|
||||||
|
path: '/app-connections/github/oauth/callback'
|
||||||
|
fullPath: '/organization/app-connections/github/oauth/callback'
|
||||||
|
preLoaderRoute: typeof organizationAppConnectionsGithubOauthCallbackPageRouteImport
|
||||||
|
parentRoute: typeof organizationLayoutImport
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1171,6 +1186,7 @@ interface organizationLayoutRouteChildren {
|
|||||||
organizationUserDetailsByIDPageRouteRoute: typeof organizationUserDetailsByIDPageRouteRoute
|
organizationUserDetailsByIDPageRouteRoute: typeof organizationUserDetailsByIDPageRouteRoute
|
||||||
organizationRoleByIDPageRouteRoute: typeof organizationRoleByIDPageRouteRoute
|
organizationRoleByIDPageRouteRoute: typeof organizationRoleByIDPageRouteRoute
|
||||||
organizationSecretManagerOverviewPageRouteRoute: typeof organizationSecretManagerOverviewPageRouteRoute
|
organizationSecretManagerOverviewPageRouteRoute: typeof organizationSecretManagerOverviewPageRouteRoute
|
||||||
|
organizationAppConnectionsGithubOauthCallbackPageRouteRoute: typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizationLayoutRouteChildren: organizationLayoutRouteChildren = {
|
const organizationLayoutRouteChildren: organizationLayoutRouteChildren = {
|
||||||
@@ -1197,6 +1213,8 @@ const organizationLayoutRouteChildren: organizationLayoutRouteChildren = {
|
|||||||
organizationRoleByIDPageRouteRoute: organizationRoleByIDPageRouteRoute,
|
organizationRoleByIDPageRouteRoute: organizationRoleByIDPageRouteRoute,
|
||||||
organizationSecretManagerOverviewPageRouteRoute:
|
organizationSecretManagerOverviewPageRouteRoute:
|
||||||
organizationSecretManagerOverviewPageRouteRoute,
|
organizationSecretManagerOverviewPageRouteRoute,
|
||||||
|
organizationAppConnectionsGithubOauthCallbackPageRouteRoute:
|
||||||
|
organizationAppConnectionsGithubOauthCallbackPageRouteRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizationLayoutRouteWithChildren =
|
const organizationLayoutRouteWithChildren =
|
||||||
@@ -1583,6 +1601,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/secret-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
|
'/secret-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
|
||||||
'/secret-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
|
'/secret-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
|
||||||
'/secret-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute
|
'/secret-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute
|
||||||
|
'/organization/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
@@ -1650,6 +1669,7 @@ export interface FileRoutesByTo {
|
|||||||
'/secret-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
|
'/secret-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
|
||||||
'/secret-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
|
'/secret-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
|
||||||
'/secret-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute
|
'/secret-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute
|
||||||
|
'/organization/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@@ -1730,6 +1750,7 @@ export interface FileRoutesById {
|
|||||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
|
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
|
||||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
|
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
|
||||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute
|
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute
|
||||||
|
'/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
@@ -1803,6 +1824,7 @@ export interface FileRouteTypes {
|
|||||||
| '/secret-manager/$projectId/identities/$identityId'
|
| '/secret-manager/$projectId/identities/$identityId'
|
||||||
| '/secret-manager/$projectId/members/$membershipId'
|
| '/secret-manager/$projectId/members/$membershipId'
|
||||||
| '/secret-manager/$projectId/roles/$roleSlug'
|
| '/secret-manager/$projectId/roles/$roleSlug'
|
||||||
|
| '/organization/app-connections/github/oauth/callback'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
@@ -1869,6 +1891,7 @@ export interface FileRouteTypes {
|
|||||||
| '/secret-manager/$projectId/identities/$identityId'
|
| '/secret-manager/$projectId/identities/$identityId'
|
||||||
| '/secret-manager/$projectId/members/$membershipId'
|
| '/secret-manager/$projectId/members/$membershipId'
|
||||||
| '/secret-manager/$projectId/roles/$roleSlug'
|
| '/secret-manager/$projectId/roles/$roleSlug'
|
||||||
|
| '/organization/app-connections/github/oauth/callback'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
@@ -1947,6 +1970,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/identities/$identityId'
|
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/identities/$identityId'
|
||||||
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/members/$membershipId'
|
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/members/$membershipId'
|
||||||
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug'
|
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug'
|
||||||
|
| '/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2181,7 +2205,8 @@ export const routeTree = rootRoute
|
|||||||
"/_authenticate/_inject-org-details/organization/_layout/kms/overview",
|
"/_authenticate/_inject-org-details/organization/_layout/kms/overview",
|
||||||
"/_authenticate/_inject-org-details/organization/_layout/members/$membershipId",
|
"/_authenticate/_inject-org-details/organization/_layout/members/$membershipId",
|
||||||
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId",
|
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId",
|
||||||
"/_authenticate/_inject-org-details/organization/_layout/secret-manager/overview"
|
"/_authenticate/_inject-org-details/organization/_layout/secret-manager/overview",
|
||||||
|
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/_authenticate/_inject-org-details/secret-manager/$projectId": {
|
"/_authenticate/_inject-org-details/secret-manager/$projectId": {
|
||||||
@@ -2388,6 +2413,10 @@ export const routeTree = rootRoute
|
|||||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug": {
|
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug": {
|
||||||
"filePath": "project/RoleDetailsBySlugPage/route-secret-manager.tsx",
|
"filePath": "project/RoleDetailsBySlugPage/route-secret-manager.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout"
|
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout"
|
||||||
|
},
|
||||||
|
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback": {
|
||||||
|
"filePath": "organization/AppConnections/GithubOauthCallbackPage/route.tsx",
|
||||||
|
"parent": "/_authenticate/_inject-org-details/organization/_layout"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ const organizationRoutes = route("/organization", [
|
|||||||
route("/groups/$groupId", "organization/GroupDetailsByIDPage/route.tsx"),
|
route("/groups/$groupId", "organization/GroupDetailsByIDPage/route.tsx"),
|
||||||
route("/members/$membershipId", "organization/UserDetailsByIDPage/route.tsx"),
|
route("/members/$membershipId", "organization/UserDetailsByIDPage/route.tsx"),
|
||||||
route("/roles/$roleId", "organization/RoleByIDPage/route.tsx"),
|
route("/roles/$roleId", "organization/RoleByIDPage/route.tsx"),
|
||||||
route("/identities/$identityId", "organization/IdentityDetailsByIDPage/route.tsx")
|
route("/identities/$identityId", "organization/IdentityDetailsByIDPage/route.tsx"),
|
||||||
|
route(
|
||||||
|
"/app-connections/github/oauth/callback",
|
||||||
|
"organization/AppConnections/GithubOauthCallbackPage/route.tsx"
|
||||||
|
)
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export type DiscriminativePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
|
||||||
Reference in New Issue
Block a user