Merge branch 'main' into ENG-3506-LDAP

This commit is contained in:
x032205
2025-09-15 22:25:18 -04:00
688 changed files with 13517 additions and 4929 deletions
@@ -21,10 +21,10 @@ import {
useAddUsersToOrg,
useFetchServerStatus,
useGetOrgRoles,
useGetUserWorkspaces
useGetUserProjects
} from "@app/hooks/api";
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { OrgInviteLink } from "./OrgInviteLink";
@@ -76,7 +76,7 @@ export const AddOrgMemberModal = ({
const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? "");
const { data: serverDetails } = useFetchServerStatus();
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
const { data: projects, isPending: isProjectsLoading } = useGetUserWorkspaces({
const { data: projects, isPending: isProjectsLoading } = useGetUserProjects({
includeRoles: true
});
@@ -1,24 +1,17 @@
import { Helmet } from "react-helmet";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, PageHeader } from "@app/components/v2";
import { PageHeader } from "@app/components/v2";
import {
OrgPermissionAppConnectionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import {
AddAppConnectionModal,
AppConnectionsTable
} from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { AppConnectionsTable } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
export const AppConnectionsPage = withPermission(
() => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addConnection"] as const);
return (
<div className="bg-bunker-800">
<Helmet>
@@ -30,54 +23,18 @@ export const AppConnectionsPage = withPermission(
<div className="w-full max-w-7xl">
<PageHeader
className="w-full"
title={
<div className="flex w-full items-center">
<span>App Connections</span>
<a
className="-mt-1.5"
href="https://infisical.com/docs/integrations/app-connections/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal 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>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.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>
}
description="Create and configure connections with third-party apps for re-use across Infisical projects"
title="App Connections"
description="Manage organization App Connections"
/>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<AppConnectionsTable />
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addConnection", isOpen)}
/>
<div className="mb-4 flex w-full flex-col rounded-md border border-blue-500/50 bg-blue-500/30 px-4 py-2 text-sm text-blue-200">
<div className="flex items-center">
<FontAwesomeIcon icon={faInfoCircle} className="mb-0.5 mr-2 text-sm" />
<span className="text-sm text-blue-200">
App connections can also be created and managed independently in projects now.
</span>
</div>
</div>
<AppConnectionsTable />
</div>
</div>
</div>
@@ -3,6 +3,7 @@ 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 { ProjectType } from "@app/hooks/api/projects/types";
import { AppConnectionForm } from "./AppConnectionForm";
import { AppConnectionsSelect } from "./AppConnectionList";
@@ -10,29 +11,44 @@ import { AppConnectionsSelect } from "./AppConnectionList";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
projectId?: string;
projectType?: ProjectType;
app?: AppConnection;
onComplete?: (appConnection: TAppConnection) => void;
};
type ContentProps = {
onComplete: (appConnection: TAppConnection) => void;
projectId?: string;
projectType?: ProjectType;
app?: AppConnection;
};
const Content = ({ onComplete }: ContentProps) => {
const Content = ({ onComplete, projectId, projectType, app }: ContentProps) => {
const [selectedApp, setSelectedApp] = useState<AppConnection | null>(null);
if (selectedApp) {
if (app ?? selectedApp) {
return (
<AppConnectionForm
onComplete={onComplete}
onBack={() => setSelectedApp(null)}
app={selectedApp}
onBack={app ? undefined : () => setSelectedApp(null)}
app={(app ?? selectedApp)!}
projectId={projectId}
/>
);
}
return <AppConnectionsSelect onSelect={setSelectedApp} />;
return <AppConnectionsSelect onSelect={setSelectedApp} projectType={projectType} />;
};
export const AddAppConnectionModal = ({ isOpen, onOpenChange }: Props) => {
export const AddAppConnectionModal = ({
isOpen,
onOpenChange,
projectId,
projectType,
app,
onComplete
}: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
@@ -40,7 +56,15 @@ export const AddAppConnectionModal = ({ isOpen, onOpenChange }: Props) => {
title="Add Connection"
subTitle="Select a third-party app to connect to."
>
<Content onComplete={() => onOpenChange(false)} />
<Content
projectId={projectId}
projectType={projectType}
app={app}
onComplete={(appConnection) => {
if (onComplete) onComplete(appConnection);
onOpenChange(false);
}}
/>
</ModalContent>
</Modal>
);
@@ -52,12 +52,15 @@ type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
} & ({ appConnection: TAppConnection } | { app: AppConnection });
type CreateFormProps = FormProps & { app: AppConnection };
type CreateFormProps = FormProps & {
app: AppConnection;
projectId?: string;
};
type UpdateFormProps = FormProps & {
appConnection: TAppConnection;
};
const CreateForm = ({ app, onComplete }: CreateFormProps) => {
const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
const createAppConnection = useCreateAppConnection();
const { name: appName } = APP_CONNECTION_MAP[app];
@@ -68,7 +71,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
>
) => {
try {
const connection = await createAppConnection.mutateAsync(formData);
const connection = await createAppConnection.mutateAsync({
...formData,
projectId
});
createNotification({
text: `Successfully added ${appName} Connection`,
type: "success"
@@ -88,15 +94,15 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.AWS:
return <AwsConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm />;
return <GitHubConnectionForm projectId={projectId} />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm />;
return <GitHubRadarConnectionForm projectId={projectId} />;
case AppConnection.GCP:
return <GcpConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} />;
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.AzureAppConfiguration:
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} />;
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.AzureADCS:
return <AzureADCSConnectionForm onSubmit={onSubmit} />;
case AppConnection.Databricks:
@@ -118,9 +124,9 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureClientSecrets:
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} />;
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.AzureDevOps:
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
return <AzureDevOpsConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.Windmill:
return <WindmillConnectionForm onSubmit={onSubmit} />;
case AppConnection.Auth0:
@@ -142,7 +148,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitLab:
return <GitLabConnectionForm onSubmit={onSubmit} />;
return <GitLabConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} />;
case AppConnection.Bitbucket:
@@ -200,16 +206,33 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.AWS:
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm appConnection={appConnection} />;
return (
<GitHubConnectionForm appConnection={appConnection} projectId={appConnection.projectId} />
);
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm appConnection={appConnection} />;
return (
<GitHubRadarConnectionForm
appConnection={appConnection}
projectId={appConnection.projectId}
/>
);
case AppConnection.GCP:
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
return <AzureKeyVaultConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
return (
<AzureKeyVaultConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.AzureAppConfiguration:
return (
<AzureAppConfigurationConnectionForm appConnection={appConnection} onSubmit={onSubmit} />
<AzureAppConfigurationConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.AzureADCS:
return <AzureADCSConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
@@ -232,9 +255,21 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.AzureClientSecrets:
return <AzureClientSecretsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
return (
<AzureClientSecretsConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.AzureDevOps:
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
return (
<AzureDevOpsConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.Windmill:
return <WindmillConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Auth0:
@@ -256,7 +291,13 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.GitLab:
return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
return (
<GitLabConnectionForm
onSubmit={onSubmit}
appConnection={appConnection}
projectId={appConnection.projectId}
/>
);
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Bitbucket:
@@ -278,12 +319,12 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
}
};
type Props = { onBack?: () => void } & Pick<FormProps, "onComplete"> &
type Props = { onBack?: () => void; projectId?: string } & Pick<FormProps, "onComplete"> &
(
| { app: AppConnection; appConnection?: undefined }
| { app?: undefined; appConnection: TAppConnection }
);
export const AppConnectionForm = ({ onBack, ...props }: Props) => {
export const AppConnectionForm = ({ onBack, projectId, ...props }: Props) => {
const { app, appConnection } = props;
return (
@@ -296,7 +337,7 @@ export const AppConnectionForm = ({ onBack, ...props }: Props) => {
{appConnection ? (
<UpdateForm {...props} appConnection={appConnection} />
) : (
<CreateForm {...props} app={app} />
<CreateForm {...props} app={app} projectId={projectId} />
)}
</div>
);
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
AzureAppConfigurationConnectionMethod,
@@ -15,6 +19,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { AzureAppConfigurationFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -25,6 +30,7 @@ type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureAppConfigurationConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -96,7 +102,11 @@ const getDefaultValues = (appConnection?: TAzureAppConfigurationConnection): Par
return base;
};
export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureAppConfigurationConnectionForm = ({
appConnection,
onSubmit,
projectId
}: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -110,6 +120,8 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }:
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -128,7 +140,12 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }:
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureAppConfigurationConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureAppConfigurationFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-app-configuration`
@@ -7,7 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
AzureClientSecretsConnectionMethod,
@@ -16,6 +20,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { AzureClientSecretsFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -26,6 +31,7 @@ type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureClientSecretsConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -97,7 +103,7 @@ const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partia
return base;
};
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -111,6 +117,8 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -129,7 +137,12 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureClientSecretsConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureClientSecretsFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://graph.microsoft.com/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
@@ -7,7 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
AzureDevOpsConnectionMethod,
@@ -16,6 +20,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { AzureDevOpsFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -65,6 +70,7 @@ type OnSubmitForm = z.infer<typeof accessTokenSchema> | z.infer<typeof clientSec
type Props = {
appConnection?: TAzureDevOpsConnection;
onSubmit: (formData: OnSubmitForm) => Promise<void>;
projectId: string | undefined | null;
};
const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormData> => {
@@ -132,7 +138,7 @@ const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormD
return base;
};
export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -146,6 +152,8 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -164,7 +172,12 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureDevOpsConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureDevOpsFormData)
);
window.location.assign(
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useGetAppConnectionOption } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
@@ -15,6 +19,7 @@ import {
TAzureKeyVaultConnection
} from "@app/hooks/api/appConnections/types/azure-key-vault-connection";
import { AzureKeyVaultFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -25,6 +30,7 @@ type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureKeyVaultConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -96,7 +102,7 @@ const getDefaultValues = (appConnection?: TAzureKeyVaultConnection): Partial<For
return base;
};
export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -110,6 +116,8 @@ export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props)
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -129,7 +137,12 @@ export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props)
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureKeyVaultConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureKeyVaultFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault`
@@ -25,7 +25,11 @@ import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { gatewaysQueryKeys } from "@app/hooks/api";
import {
@@ -35,6 +39,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { GithubFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -42,6 +47,7 @@ import {
type Props = {
appConnection?: TGitHubConnection;
projectId: string | undefined | null;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
@@ -63,7 +69,7 @@ const formSchema = genericAppConnectionFieldsSchema.extend({
type FormData = z.infer<typeof formSchema>;
export const GitHubConnectionForm = ({ appConnection }: Props) => {
export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -98,13 +104,21 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
const selectedMethod = watch("method");
const instanceType = watch("credentials.instanceType");
const returnUrl = useGetAppConnectionOauthReturnUrl();
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 })
JSON.stringify({
...formData,
credentials: formData.credentials as TGitHubConnection["credentials"],
connectionId: appConnection?.id,
projectId,
returnUrl
} as GithubFormData)
);
const githubHost =
@@ -6,7 +6,11 @@ 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 {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
GitHubRadarConnectionMethod,
@@ -15,6 +19,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { GithubRadarFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -22,6 +27,7 @@ import {
type Props = {
appConnection?: TGitHubRadarConnection;
projectId: string | undefined | null;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
@@ -31,7 +37,7 @@ const formSchema = genericAppConnectionFieldsSchema.extend({
type FormData = z.infer<typeof formSchema>;
export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
export const GitHubRadarConnectionForm = ({ appConnection, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -48,6 +54,8 @@ export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
}
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -63,7 +71,12 @@ export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"githubRadarConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as GithubRadarFormData)
);
switch (formData.method) {
@@ -16,7 +16,11 @@ import {
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useGetAppConnectionOption } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
@@ -26,6 +30,7 @@ import {
TGitLabConnection
} from "@app/hooks/api/appConnections/types/gitlab-connection";
import { GitLabFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -34,6 +39,7 @@ import {
type Props = {
appConnection?: TGitLabConnection;
onSubmit: (formData: FormData) => Promise<void>;
projectId: string | undefined | null;
};
const formSchema = z.discriminatedUnion("method", [
@@ -72,7 +78,7 @@ const formSchema = z.discriminatedUnion("method", [
type FormData = z.infer<typeof formSchema>;
export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Props) => {
export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -98,6 +104,8 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
} as FormData))
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -132,8 +140,10 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
JSON.stringify({
...formData,
connectionId: appConnection?.id,
isUpdate
})
isUpdate,
projectId,
returnUrl
} as GitLabFormData)
);
// Redirect to Gitlab OAuth
@@ -9,14 +9,16 @@ import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useAppConnectionOptions } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { ProjectType } from "@app/hooks/api/projects/types";
type Props = {
onSelect: (app: AppConnection) => void;
projectType?: ProjectType;
};
export const AppConnectionsSelect = ({ onSelect }: Props) => {
export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => {
const { subscription } = useSubscription();
const { isPending, data: appConnectionOptions } = useAppConnectionOptions();
const { isPending, data: appConnectionOptions } = useAppConnectionOptions(projectType);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -1,19 +1,23 @@
import { useCallback } from "react";
import { subject } from "@casl/ability";
import {
faAsterisk,
faBuilding,
faCheck,
faCopy,
faEdit,
faEllipsisV,
faInfoCircle,
faServer,
faTable,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { VariablePermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
@@ -25,9 +29,11 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionSubjects } from "@app/context";
import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { getProjectBaseURL } from "@app/helpers/project";
import { useToggle } from "@app/hooks";
import { TAppConnection } from "@app/hooks/api/appConnections";
@@ -36,15 +42,18 @@ type Props = {
onDelete: (appConnection: TAppConnection) => void;
onEditCredentials: (appConnection: TAppConnection) => void;
onEditDetails: (appConnection: TAppConnection) => void;
isProjectView: boolean;
};
export const AppConnectionRow = ({
appConnection,
onDelete,
onEditCredentials,
onEditDetails
onEditDetails,
isProjectView
}: Props) => {
const { id, name, method, app, description, isPlatformManagedCredentials } = appConnection;
const { id, name, method, app, description, isPlatformManagedCredentials, project } =
appConnection;
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -111,7 +120,38 @@ export const AppConnectionRow = ({
{methodDetails.name}
</p>
</Td>
{!isProjectView && (
<Td className="!min-w-[8rem] max-w-0">
{project ? (
<Link
// @ts-expect-error app-connections aren't in kms/ssh
to={`${getProjectBaseURL(project.type)}/app-connections`}
params={{
projectId: project.id
}}
className="underline"
>
<p className="truncate">
<FontAwesomeIcon
size="sm"
className="mr-1.5 text-mineshaft-300/75"
icon={faTable}
/>
{project.name}
</p>
</Link>
) : (
<p className="truncate">
<FontAwesomeIcon
size="sm"
className="mr-1.5 text-mineshaft-300/75"
icon={faBuilding}
/>
Organization
</p>
)}
</Td>
)}
<Td>
<div className="flex items-center justify-end gap-2">
{isPlatformManagedCredentials && (
@@ -143,48 +183,91 @@ export const AppConnectionRow = ({
>
Copy Connection ID
</DropdownMenuItem>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Edit}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onEditDetails(appConnection)}
{(isProjectView || !project) && (
<>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Edit
: OrgPermissionAppConnectionActions.Edit
}
a={
isProjectView
? subject(ProjectPermissionSub.AppConnections, {
connectionId: id
})
: subject(OrgPermissionSubjects.AppConnections, {
connectionId: id
})
}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Edit}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faAsterisk} />}
onClick={() => onEditCredentials(appConnection)}
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onEditDetails(appConnection)}
>
Edit Details
</DropdownMenuItem>
)}
</VariablePermissionCan>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Edit
: OrgPermissionAppConnectionActions.Edit
}
a={
isProjectView
? subject(ProjectPermissionSub.AppConnections, {
connectionId: id
})
: subject(OrgPermissionSubjects.AppConnections, {
connectionId: id
})
}
>
{isPlatformManagedCredentials ? "View" : "Edit"} Credentials
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Delete}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(appConnection)}
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faAsterisk} />}
onClick={() => onEditCredentials(appConnection)}
>
{isPlatformManagedCredentials ? "View" : "Edit"} Credentials
</DropdownMenuItem>
)}
</VariablePermissionCan>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Delete
: OrgPermissionAppConnectionActions.Delete
}
a={
isProjectView
? subject(ProjectPermissionSub.AppConnections, {
connectionId: id
})
: subject(OrgPermissionSubjects.AppConnections, {
connectionId: id
})
}
>
Delete Connection
</DropdownMenuItem>
)}
</OrgPermissionCan>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(appConnection)}
>
Delete Connection
</DropdownMenuItem>
)}
</VariablePermissionCan>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
@@ -2,16 +2,21 @@ import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faArrowUpRightFromSquare,
faBookOpen,
faCheckCircle,
faFilter,
faMagnifyingGlass,
faPlug,
faPlus,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { VariablePermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -29,6 +34,9 @@ import {
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
getUserTablePreference,
@@ -39,7 +47,9 @@ 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 { ProjectType } from "@app/hooks/api/projects/types";
import { AddAppConnectionModal } from "./AddAppConnectionModal";
import { AppConnectionRow } from "./AppConnectionRow";
import { DeleteAppConnectionModal } from "./DeleteAppConnectionModal";
import { EditAppConnectionCredentialsModal } from "./EditAppConnectionCredentialsModal";
@@ -48,22 +58,45 @@ import { EditAppConnectionDetailsModal } from "./EditAppConnectionDetailsModal";
enum AppConnectionsOrderBy {
App = "app",
Name = "name",
Method = "method"
Method = "method",
ManagedBy = "managed-by"
}
type AppConnectionFilters = {
apps: AppConnection[];
};
export const AppConnectionsTable = () => {
const { isPending, data: appConnections = [] } = useListAppConnections();
enum View {
All = "all",
Scope = "scope"
}
const APP_CONNECTION_VIEW_STORAGE_KEY = "app-connection-view";
type Props = {
projectId?: string;
projectType?: ProjectType;
};
export const AppConnectionsTable = ({ projectId, projectType }: Props) => {
const isProjectView = Boolean(projectId);
const { isPending, data: appConnections = [] } = useListAppConnections(projectId);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"addConnection",
"deleteConnection",
"editCredentials",
"editDetails"
] as const);
const [view, setView] = useState<View>(() => {
const storedView = localStorage.getItem(APP_CONNECTION_VIEW_STORAGE_KEY) as View | null;
if (storedView && Object.values(View).includes(storedView)) return storedView;
return View.Scope;
});
const [filters, setFilters] = useState<AppConnectionFilters>({
apps: []
});
@@ -96,6 +129,10 @@ export const AppConnectionsTable = () => {
.filter((appConnection) => {
const { app, method, name } = appConnection;
if (view === View.Scope && !isProjectView && appConnection.projectId) {
return false;
}
if (filters.apps.length && !filters.apps.includes(app)) return false;
const searchValue = search.trim().toLowerCase();
@@ -121,6 +158,13 @@ export const AppConnectionsTable = () => {
.localeCompare(
getAppConnectionMethodDetails(connectionTwo.method).name.toLowerCase()
);
case AppConnectionsOrderBy.ManagedBy:
if (!connectionOne.project) return 1;
if (!connectionTwo.project) return -1;
return connectionOne.project.name
.toLowerCase()
.localeCompare(connectionTwo.project.name.toLowerCase());
case AppConnectionsOrderBy.App:
default:
return APP_CONNECTION_MAP[connectionOne.app].name
@@ -128,7 +172,7 @@ export const AppConnectionsTable = () => {
.localeCompare(APP_CONNECTION_MAP[connectionTwo.app].name.toLowerCase());
}
}),
[appConnections, orderDirection, search, orderBy, filters]
[appConnections, orderDirection, search, orderBy, filters, view]
);
useResetPageHelper({
@@ -165,8 +209,87 @@ export const AppConnectionsTable = () => {
handlePopUpOpen("editDetails", appConnection);
return (
<div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-center 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.12rem] 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 your project
{isProjectView ? "" : "s"}.
</p>
</div>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Create
: OrgPermissionAppConnectionActions.Create
}
a={
isProjectView
? ProjectPermissionSub.AppConnections
: OrgPermissionSubjects.AppConnections
}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addConnection")}
isDisabled={!isAllowed}
>
Add Connection
</Button>
)}
</VariablePermissionCan>
</div>
<div className="flex gap-2">
{!isProjectView && (
<div className="flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<Button
variant="outline_bg"
onClick={() => {
setView(View.Scope);
localStorage.setItem(APP_CONNECTION_VIEW_STORAGE_KEY, View.Scope);
}}
size="xs"
className={`${
view === View.Scope ? "bg-mineshaft-500" : "bg-transparent"
} min-w-[5rem] rounded border-none hover:bg-mineshaft-600`}
>
Organization
</Button>
<Button
variant="outline_bg"
onClick={() => {
setView(View.All);
localStorage.setItem(APP_CONNECTION_VIEW_STORAGE_KEY, View.All);
}}
size="xs"
className={`${
view === View.All ? "bg-mineshaft-500" : "bg-transparent"
} min-w-[5rem] rounded border-none hover:bg-mineshaft-600`}
>
All
</Button>
</div>
)}
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
@@ -188,38 +311,46 @@ export const AppConnectionsTable = () => {
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
<DropdownMenuContent
sideOffset={2}
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>
))
[...new Set(appConnections.map(({ app }) => app))]
.sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase());
})
.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>
)}
@@ -269,7 +400,21 @@ export const AppConnectionsTable = () => {
</IconButton>
</div>
</Th>
{!isProjectView && (
<Th>
<div className="flex items-center">
Managed By
<IconButton
variant="plain"
className={getClassName(AppConnectionsOrderBy.ManagedBy)}
ariaLabel="sort"
onClick={() => handleSort(AppConnectionsOrderBy.ManagedBy)}
>
<FontAwesomeIcon icon={getColSortIcon(AppConnectionsOrderBy.ManagedBy)} />
</IconButton>
</div>
</Th>
)}
<Th className="w-5" />
</Tr>
</THead>
@@ -284,6 +429,7 @@ export const AppConnectionsTable = () => {
onDelete={handleDelete}
onEditCredentials={handleEditCredentials}
onEditDetails={handleEditDetails}
isProjectView={isProjectView}
/>
))}
</TBody>
@@ -323,6 +469,12 @@ export const AppConnectionsTable = () => {
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
appConnection={popUp.editDetails.data}
/>
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addConnection", isOpen)}
projectId={projectId}
projectType={projectType}
/>
</div>
);
};
@@ -12,73 +12,14 @@ import {
AzureKeyVaultConnectionMethod,
GitHubConnectionMethod,
GitLabConnectionMethod,
TAzureAppConfigurationConnection,
TAzureClientSecretsConnection,
TAzureDevOpsConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
TGitHubRadarConnection,
TGitLabConnection,
TAppConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { IntegrationsListPageTabs } from "@app/types/integrations";
type BaseFormData = {
returnUrl?: string;
connectionId?: string;
isUpdate?: boolean;
};
type GithubFormData = BaseFormData &
Pick<TGitHubConnection, "name" | "method" | "description" | "gatewayId" | "credentials">;
type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
type GitLabFormData = BaseFormData &
Pick<TGitLabConnection, "name" | "method" | "description" | "credentials">;
type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
type AzureAppConfigurationFormData = BaseFormData &
Pick<TAzureAppConfigurationConnection, "name" | "method" | "description"> &
Pick<TAzureAppConfigurationConnection["credentials"], "tenantId">;
type AzureClientSecretsFormData = BaseFormData &
Pick<TAzureClientSecretsConnection, "name" | "method" | "description"> &
Pick<TAzureClientSecretsConnection["credentials"], "tenantId">;
type OAuthCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.OAuth }
>["credentials"];
type AccessTokenCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.AccessToken }
>["credentials"];
type AzureDevOpsFormData = BaseFormData &
Pick<TAzureDevOpsConnection, "name" | "method" | "description"> &
(Pick<OAuthCredentials, "tenantId" | "orgName"> | Pick<AccessTokenCredentials, "orgName">);
type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
[AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
};
[AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
app: AppConnection.AzureClientSecrets;
};
[AppConnection.AzureDevOps]: AzureDevOpsFormData & {
app: AppConnection.AzureDevOps;
};
};
import { FormDataMap } from "./OauthCallbackPage.types";
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
@@ -148,11 +89,14 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitLab);
const { connectionId, name, description, returnUrl, isUpdate, credentials } = formData;
const { connectionId, name, description, returnUrl, isUpdate, projectId, credentials } =
formData;
let connection: TAppConnection;
try {
if (isUpdate && connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitLab,
connectionId,
credentials: {
@@ -161,10 +105,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.GitLab,
name,
description,
projectId,
method: GitLabConnectionMethod.OAuth,
credentials: {
code: code as string,
@@ -173,14 +118,12 @@ export const OAuthCallbackPage = () => {
});
}
navigate({
to: returnUrl ?? "/organization/app-connections"
});
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
} catch (err: any) {
createNotification({
@@ -189,7 +132,10 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
@@ -201,11 +147,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureKeyVault);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureKeyVault,
connectionId,
credentials: {
@@ -214,10 +162,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureKeyVault,
name,
description,
projectId,
method: AzureKeyVaultConnectionMethod.OAuth,
credentials: {
tenantId: formData.tenantId,
@@ -232,14 +181,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -249,11 +204,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureAppConfiguration);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureAppConfiguration,
connectionId,
credentials: {
@@ -262,10 +219,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureAppConfiguration,
name,
description,
projectId,
method: AzureAppConfigurationConnectionMethod.OAuth,
credentials: {
code: code as string,
@@ -280,14 +238,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -297,11 +261,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureClientSecrets);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureClientSecrets,
connectionId,
credentials: {
@@ -310,11 +276,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureClientSecrets,
name,
description,
method: AzureClientSecretsConnectionMethod.OAuth,
projectId,
credentials: {
code: code as string,
tenantId: formData.tenantId
@@ -328,14 +295,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -345,7 +318,9 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureDevOps);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (!("tenantId" in formData)) {
@@ -353,7 +328,7 @@ export const OAuthCallbackPage = () => {
}
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureDevOps,
connectionId,
credentials: {
@@ -363,11 +338,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureDevOps,
name,
description,
method: AzureDevOpsConnectionMethod.OAuth,
projectId,
credentials: {
code: code as string,
tenantId: formData.tenantId as string,
@@ -382,14 +358,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -399,11 +381,14 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitHub);
const { connectionId, name, description, returnUrl, gatewayId, credentials } = formData;
const { connectionId, name, description, returnUrl, gatewayId, credentials, projectId } =
formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitHub,
...(installationId
? {
@@ -427,10 +412,11 @@ export const OAuthCallbackPage = () => {
})
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.GitHub,
name,
description,
projectId,
...(installationId
? {
method: GitHubConnectionMethod.App,
@@ -460,14 +446,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -477,11 +469,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitHubRadar);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
connectionId,
credentials: {
@@ -490,11 +484,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
name,
description,
method: GitHubConnectionMethod.App,
projectId,
credentials: {
code: code as string,
installationId: installationId as string
@@ -508,14 +503,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -530,8 +531,13 @@ export const OAuthCallbackPage = () => {
if (!isReady) return;
(async () => {
let data: { connectionId?: string; returnUrl?: string; appConnectionName?: string } | null =
null;
let data: {
returnUrl: string;
appConnectionName: string;
connectionId?: string;
projectId?: string;
connection: TAppConnection;
} | null = null;
if (appConnection === AppConnection.GitHub) {
data = await handleGithub();
@@ -554,16 +560,26 @@ export const OAuthCallbackPage = () => {
text: `Successfully ${data.connectionId ? "updated" : "added"} ${data.appConnectionName ? APP_CONNECTION_MAP[data.appConnectionName as AppConnection].name : ""} Connection`,
type: "success"
});
} else {
createNotification({
text: "Failed to add connection",
type: "error"
await navigate({
to: data.returnUrl,
params: {
projectId: data.projectId ?? undefined
},
// scott: if it's not an app connection page we need to pass connection details as it's an inline creation
search: data.returnUrl.includes("app-connections")
? undefined
: {
connectionId: data.connection.id,
connectionName: data.connection.name,
...(data.returnUrl.includes("integrations")
? {
selectedTab: IntegrationsListPageTabs.SecretSyncs
}
: {})
}
});
}
await navigate({
to: data?.returnUrl ?? "/organization/app-connections"
});
})();
}, [isReady]);
@@ -0,0 +1,68 @@
import {
AzureDevOpsConnectionMethod,
TAzureAppConfigurationConnection,
TAzureClientSecretsConnection,
TAzureDevOpsConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
TGitHubRadarConnection,
TGitLabConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
type BaseFormData = {
returnUrl: string;
connectionId?: string;
isUpdate?: boolean;
projectId: string;
};
export type GithubFormData = BaseFormData &
Pick<TGitHubConnection, "name" | "method" | "description" | "gatewayId" | "credentials">;
export type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
export type GitLabFormData = BaseFormData &
Pick<TGitLabConnection, "name" | "method" | "description" | "credentials">;
export type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
export type AzureAppConfigurationFormData = BaseFormData &
Pick<TAzureAppConfigurationConnection, "name" | "method" | "description"> &
Pick<TAzureAppConfigurationConnection["credentials"], "tenantId">;
export type AzureClientSecretsFormData = BaseFormData &
Pick<TAzureClientSecretsConnection, "name" | "method" | "description"> &
Pick<TAzureClientSecretsConnection["credentials"], "tenantId">;
type OAuthCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.OAuth }
>["credentials"];
type AccessTokenCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.AccessToken }
>["credentials"];
export type AzureDevOpsFormData = BaseFormData &
Pick<TAzureDevOpsConnection, "name" | "method" | "description"> &
(Pick<OAuthCredentials, "tenantId" | "orgName"> | Pick<AccessTokenCredentials, "orgName">);
export type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
[AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
};
[AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
app: AppConnection.AzureClientSecrets;
};
[AppConnection.AzureDevOps]: AzureDevOpsFormData & {
app: AppConnection.AzureDevOps;
};
};
@@ -21,7 +21,7 @@ import {
SelectItem
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { useGetUserProjects } from "@app/hooks/api";
import {
eventToNameMap,
secretEvents,
@@ -29,7 +29,7 @@ import {
} from "@app/hooks/api/auditLogs/constants";
import { EventType } from "@app/hooks/api/auditLogs/enums";
import { UserAgentType } from "@app/hooks/api/auth/types";
import { Workspace } from "@app/hooks/api/workspace/types";
import { Project } from "@app/hooks/api/projects/types";
import { LogFilterItem } from "./LogFilterItem";
import { auditLogFilterFormSchema, Presets, TAuditLogFilterFormData } from "./types";
@@ -44,7 +44,7 @@ type Props = {
presets?: Presets;
setFilter: (data: TAuditLogFilterFormData) => void;
filter: TAuditLogFilterFormData;
project?: Workspace;
project?: Project;
};
const getActiveFilterCount = (filter: TAuditLogFilterFormData) => {
@@ -72,7 +72,7 @@ const getActiveFilterCount = (filter: TAuditLogFilterFormData) => {
};
export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
const { data: workspaces = [] } = useGetUserWorkspaces();
const { data: workspaces = [] } = useGetUserProjects();
const { currentOrg } = useOrganization();
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
@@ -13,7 +13,7 @@ import {
} from "@app/context";
import { Timezone } from "@app/helpers/datetime";
import { withPermission, withProjectPermission } from "@app/hoc";
import { Workspace } from "@app/hooks/api/workspace/types";
import { Project } from "@app/hooks/api/projects/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { LogsDateFilter } from "./LogsDateFilter";
@@ -31,7 +31,7 @@ type Props = {
refetchInterval?: number;
showFilters?: boolean;
pageView?: boolean;
project?: Workspace;
project?: Project;
};
const LogsSectionComponent = ({
@@ -1,7 +1,7 @@
import { z } from "zod";
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectType } from "@app/hooks/api/projects/types";
export enum AuditLogDateFilterType {
Relative = "relative",
@@ -4,11 +4,11 @@ import {
faArrowUpRightFromSquare,
faBookOpen,
faCopy,
faDoorClosed,
faEdit,
faEllipsisV,
faInfoCircle,
faMagnifyingGlass,
faPlug,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
@@ -243,7 +243,7 @@ export const GatewayListPage = withPermission(
? "No Gateways match search..."
: "No Gateways have been configured"
}
icon={gateways?.length ? faSearch : faPlug}
icon={gateways?.length ? faSearch : faDoorClosed}
/>
)}
<DeleteActionModal
@@ -17,7 +17,7 @@ import {
useAddIdentityToWorkspace,
useGetIdentityProjectMemberships,
useGetProjectRoles,
useGetUserWorkspaces,
useGetUserProjects,
useGetWorkspaceById
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -44,7 +44,7 @@ type Props = {
const Content = ({ identityId, handlePopUpToggle }: Omit<Props, "popUp">) => {
const { currentOrg } = useOrganization();
const { data: workspaces = [] } = useGetUserWorkspaces();
const { data: workspaces = [] } = useGetUserProjects();
const { mutateAsync: addIdentityToWorkspace } = useAddIdentityToWorkspace();
const {
@@ -77,7 +77,7 @@ const Content = ({ identityId, handlePopUpToggle }: Omit<Props, "popUp">) => {
const onFormSubmit = async ({ project: selectedProject, role }: FormData) => {
try {
await addIdentityToWorkspace({
workspaceId: selectedProject.id,
projectId: selectedProject.id,
identityId,
role: role.slug || undefined
});
@@ -8,7 +8,7 @@ import { createNotification } from "@app/components/notifications";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { getProjectBaseURL } from "@app/helpers/project";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { useGetUserProjects } from "@app/hooks/api";
import { IdentityMembership } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -31,7 +31,7 @@ export const IdentityProjectRow = ({
membership: { id, createdAt, identity, project, roles },
handlePopUpOpen
}: Props) => {
const { data: workspaces } = useGetUserWorkspaces();
const { data: workspaces } = useGetUserProjects();
const navigate = useNavigate();
const isAccessible = useMemo(() => {
@@ -25,7 +25,7 @@ export const IdentityProjectsSection = ({ identityId }: Props) => {
try {
await deleteMutateAsync({
identityId: id,
workspaceId: projectId
projectId
});
createNotification({
@@ -1,5 +1,4 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import {
faArrowDownAZ,
faBorderAll,
@@ -16,6 +15,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { RequestProjectAccessModal } from "@app/components/projects/RequestProjectAccessModal";
import {
Badge,
Button,
@@ -24,12 +24,9 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
FormControl,
IconButton,
Input,
Lottie,
Modal,
ModalContent,
Pagination,
Skeleton,
Tooltip
@@ -43,12 +40,8 @@ import {
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import {
useOrgAdminAccessProject,
useRequestProjectAccess,
useSearchProjects
} from "@app/hooks/api";
import { ProjectType, Workspace, WorkspaceEnv } from "@app/hooks/api/workspace/types";
import { useOrgAdminAccessProject, useSearchProjects } from "@app/hooks/api";
import { Project, ProjectEnv, ProjectType } from "@app/hooks/api/projects/types";
import {
ProjectListToggle,
ProjectListView
@@ -62,53 +55,6 @@ type Props = {
onProjectListViewChange: (value: ProjectListView) => void;
};
type RequestAccessModalProps = {
projectId: string;
onPopUpToggle: () => void;
};
const RequestAccessModal = ({ projectId, onPopUpToggle }: RequestAccessModalProps) => {
const form = useForm<{ note: string }>();
const requestProjectAccess = useRequestProjectAccess();
const onFormSubmit = ({ note }: { note: string }) => {
if (requestProjectAccess.isPending) return;
requestProjectAccess.mutate(
{
comment: note,
projectId
},
{
onSuccess: () => {
createNotification({
type: "success",
title: "Project Access Request Sent",
text: "Project admins will receive an email of your request"
});
onPopUpToggle();
}
}
);
};
return (
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<FormControl label="Note">
<Input {...form.register("note")} />
</FormControl>
<div className="mt-4 flex items-center">
<Button className="mr-4" size="sm" type="submit" isLoading={form.formState.isSubmitting}>
Submit Request
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => onPopUpToggle()}>
Cancel
</Button>
</div>
</form>
);
};
export const AllProjectView = ({
onAddNewProject,
onUpgradePlan,
@@ -155,7 +101,7 @@ export const AllProjectView = ({
const handleAccessProject = async (
type: ProjectType,
projectId: string,
environments: WorkspaceEnv[]
environments: ProjectEnv[]
) => {
try {
await orgAdminAccessProject.mutateAsync({
@@ -180,7 +126,7 @@ export const AllProjectView = ({
offset,
totalCount: searchedProjects?.totalCount || 0
});
const requestedWorkspaceDetails = (popUp.requestAccessConfirmation.data || {}) as Workspace;
const requestedWorkspaceDetails = (popUp.requestAccessConfirmation.data || {}) as Project;
const handleToggleFilterByProjectType = (el: ProjectType) =>
setProjectTypeFilter((state) => (state === el ? undefined : el));
@@ -278,22 +224,26 @@ export const AllProjectView = ({
</IconButton>
</div>
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Workspace}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
onAddNewProject();
} else {
onUpgradePlan();
}
}}
className="ml-2"
>
Add New Project
</Button>
{(isOldProjectPermissionAllowed) => (
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Project}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed && !isOldProjectPermissionAllowed}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
onAddNewProject();
} else {
onUpgradePlan();
}
}}
className="ml-2"
>
Add New Project
</Button>
)}
</OrgPermissionCan>
)}
</OrgPermissionCan>
</div>
@@ -419,20 +369,11 @@ export const AllProjectView = ({
<div className="text-center font-light">No Projects Found</div>
</div>
)}
<Modal
<RequestProjectAccessModal
isOpen={popUp.requestAccessConfirmation.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("requestAccessConfirmation", isOpen)}
>
<ModalContent
title="Confirm Access Request"
subTitle={`Requesting access to project ${requestedWorkspaceDetails?.name}. You may include an optional note for project admins to review your request.`}
>
<RequestAccessModal
onPopUpToggle={() => handlePopUpToggle("requestAccessConfirmation")}
projectId={requestedWorkspaceDetails?.id}
/>
</ModalContent>
</Modal>
project={requestedWorkspaceDetails}
/>
</div>
);
};
@@ -39,11 +39,11 @@ import {
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { useGetUserProjects } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { Project, ProjectType } from "@app/hooks/api/projects/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
import {
ProjectListToggle,
ProjectListView
@@ -79,7 +79,7 @@ export const MyProjectView = ({
{}
);
const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserWorkspaces();
const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserProjects();
const {
setPage,
perPage,
@@ -136,7 +136,7 @@ export const MyProjectView = ({
const { workspacesWithFaveProp } = useMemo(() => {
const workspacesWithFav = filteredWorkspaces
.map((w): Workspace & { isFavorite: boolean } => ({
.map((w): Project & { isFavorite: boolean } => ({
...w,
isFavorite: Boolean(projectFavorites?.includes(w.id))
}))
@@ -188,7 +188,7 @@ export const MyProjectView = ({
}
};
const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => (
const renderProjectGridItem = (workspace: Project, isFavorite: boolean) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
onClick={() => {
@@ -242,7 +242,7 @@ export const MyProjectView = ({
</p>
</div>
);
const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => (
const renderProjectListItem = (workspace: Project, isFavorite: boolean, index: number) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
onClick={() => {
@@ -480,22 +480,26 @@ export const MyProjectView = ({
</IconButton>
</div>
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Workspace}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
onAddNewProject();
} else {
onUpgradePlan();
}
}}
className="ml-2"
>
Add New Project
</Button>
{(isOldProjectV1Allowed) => (
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Project}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed && !isOldProjectV1Allowed}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
onAddNewProject();
} else {
onUpgradePlan();
}
}}
className="ml-2"
>
Add New Project
</Button>
)}
</OrgPermissionCan>
)}
</OrgPermissionCan>
</div>
@@ -122,12 +122,11 @@ export const formSchema = z.object({
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
permissions: z
.object({
workspace: z
project: z
.object({
create: z.boolean().optional()
})
.optional(),
"audit-logs": auditLogsPermissionSchema,
member: generalPermissionSchema,
groups: groupPermissionSchema,
@@ -162,7 +161,11 @@ export const rolePermission2Form = (permissions: TPermission[] = []) => {
// i would have to write a if loop with both conditions same
const formVal: Record<string, any> = {};
permissions.forEach((permission) => {
const { subject, action } = permission;
const { action } = permission;
let { subject } = permission;
if (subject === OrgPermissionSubjects.Workspace) {
subject = OrgPermissionSubjects.Project;
}
if (!formVal?.[subject]) formVal[subject] = {};
formVal[subject][action] = true;
});
@@ -28,7 +28,7 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) =>
const rule = useWatch({
control,
name: "permissions.workspace"
name: "permissions.project"
});
const selectedPermissionCategory = useMemo(() => {
@@ -60,7 +60,7 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) =>
setIsCustom.off();
if (val === Permission.NoAccess) {
setValue("permissions.workspace", { create: false }, { shouldDirty: true });
setValue("permissions.project", { create: false }, { shouldDirty: true });
}
};
@@ -95,8 +95,8 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) =>
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.workspace.${action}`}
key={`permissions.workspace.${action}`}
name={`permissions.project.${action}`}
key={`permissions.project.${action}`}
control={control}
render={({ field }) => (
<Checkbox
@@ -65,7 +65,7 @@ type Props = {
title: string;
formName: keyof Omit<
Exclude<TFormSchema["permissions"], undefined>,
| "workspace"
| "project"
| "organization-admin-console"
| "kmip"
| "gateway"
@@ -1,9 +1,8 @@
import { faArrowUpRightFromSquare, faBookOpen, faPlus } 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, useOrgPermission } from "@app/context";
import { useOrgPermission } from "@app/context";
import { usePopUp } from "@app/hooks";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
@@ -15,52 +14,45 @@ export const ExternalMigrationsTab = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const);
return (
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.Workspace}
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 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>
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/external-migrations/overview"
>
<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" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-xxs"
/>
</div>
</a>
</div>
<div>
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/external-migrations/overview"
>
<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" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-xxs"
/>
</div>
</a>
</div>
<Button
onClick={() => {
handlePopUpOpen("selectImportPlatform");
}}
isDisabled={membership?.role !== ProjectMembershipRole.Admin}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Import
</Button>
</div>
<p className="mb-4 text-gray-400">Import data from another platform to Infisical.</p>
<SelectImportFromPlatformModal
isOpen={popUp.selectImportPlatform.isOpen}
onToggle={(state) => handlePopUpToggle("selectImportPlatform", state)}
/>
<Button
onClick={() => {
handlePopUpOpen("selectImportPlatform");
}}
isDisabled={membership?.role !== ProjectMembershipRole.Admin}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Import
</Button>
</div>
</OrgPermissionCan>
<p className="mb-4 text-gray-400">Import data from another platform to Infisical.</p>
<SelectImportFromPlatformModal
isOpen={popUp.selectImportPlatform.isOpen}
onToggle={(state) => handlePopUpToggle("selectImportPlatform", state)}
/>
</div>
);
};
@@ -7,8 +7,8 @@ import { Button, DeleteActionModal } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/projects/types";
import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal";
import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm";
@@ -15,12 +15,12 @@ import {
TextArea
} from "@app/components/v2";
import { getProjectLottieIcon } from "@app/helpers/project";
import { ProjectType } from "@app/hooks/api/projects/types";
import {
TProjectTemplate,
useCreateProjectTemplate,
useUpdateProjectTemplate
} from "@app/hooks/api/projectTemplates";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({
@@ -9,9 +9,9 @@ import { useOrganization } from "@app/context";
import {
useAddUserToWsNonE2EE,
useGetOrgMembershipProjectMemberships,
useGetUserWorkspaces
useGetUserProjects
} from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { ProjectVersion } from "@app/hooks/api/projects/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
@@ -34,7 +34,7 @@ type Props = {
const UserAddToProjectModalChild = ({ membershipId, popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data: workspaces = [] } = useGetUserWorkspaces();
const { data: workspaces = [] } = useGetUserProjects();
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
@@ -7,7 +7,7 @@ import { createNotification } from "@app/components/notifications";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { getProjectBaseURL } from "@app/helpers/project";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { useGetUserProjects } from "@app/hooks/api";
import { TWorkspaceUser } from "@app/hooks/api/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { OrgAccessControlTabSections } from "@app/types/org";
@@ -24,7 +24,7 @@ export const UserProjectRow = ({
membership: { id, project, user, roles },
handlePopUpOpen
}: Props) => {
const { data: workspaces = [] } = useGetUserWorkspaces();
const { data: workspaces = [] } = useGetUserProjects();
const navigate = useNavigate();
const isAccessible = useMemo(() => {
@@ -32,7 +32,7 @@ export const UserProjectsSection = ({ membershipId }: Props) => {
const handleRemoveUser = async (projectId: string, username: string) => {
try {
await removeUserFromWorkspace({ workspaceId: projectId, usernames: [username], orgId });
await removeUserFromWorkspace({ projectId, usernames: [username], orgId });
createNotification({
text: "Successfully removed user from project",
type: "success"