mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 19:28:16 +00:00
improvement: remove overview page and re-vamp secret dashboard
This commit is contained in:
@@ -153,7 +153,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
|||||||
reset();
|
reset();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(project.type),
|
to: getProjectHomePage(project.type, project.environments),
|
||||||
params: { projectId: project.id }
|
params: { projectId: project.id }
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type Props = {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
icon?: IconDefinition;
|
icon?: IconDefinition;
|
||||||
iconSize?: SizeProp;
|
iconSize?: SizeProp;
|
||||||
|
titleClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmptyState = ({
|
export const EmptyState = ({
|
||||||
@@ -17,7 +18,8 @@ export const EmptyState = ({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
icon = faCubesStacked,
|
icon = faCubesStacked,
|
||||||
iconSize = "2x"
|
iconSize = "2x",
|
||||||
|
titleClassName
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@@ -27,7 +29,7 @@ export const EmptyState = ({
|
|||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={icon} size={iconSize} />
|
<FontAwesomeIcon icon={icon} size={iconSize} />
|
||||||
<div className="flex flex-col items-center py-4">
|
<div className="flex flex-col items-center py-4">
|
||||||
<div className="text-sm text-bunker-300">{title}</div>
|
<div className={twMerge("text-sm text-bunker-300", titleClassName)}>{title}</div>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const FilterableSelect = <T,>({
|
|||||||
}),
|
}),
|
||||||
menuPortal: (provided) => ({
|
menuPortal: (provided) => ({
|
||||||
...provided,
|
...provided,
|
||||||
zIndex: 9999
|
zIndex: 99999
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
tabSelectsValue={tabSelectsValue}
|
tabSelectsValue={tabSelectsValue}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { createWorkspace } from "@app/hooks/api/workspace/queries";
|
import { createWorkspace } from "@app/hooks/api/workspace/queries";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType, WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
const secretsToBeAdded = [
|
const secretsToBeAdded = [
|
||||||
{
|
{
|
||||||
@@ -72,9 +72,12 @@ export const getProjectBaseURL = (type: ProjectType) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProjectHomePage = (type: ProjectType) => {
|
export const getProjectHomePage = (type: ProjectType, environments: WorkspaceEnv[]) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ProjectType.SecretManager:
|
case ProjectType.SecretManager:
|
||||||
|
if (environments.length > 0)
|
||||||
|
return `/projects/secret-management/$projectId/secrets/${environments[0].slug}`;
|
||||||
|
|
||||||
return "/projects/secret-management/$projectId/overview";
|
return "/projects/secret-management/$projectId/overview";
|
||||||
case ProjectType.CertificateManager:
|
case ProjectType.CertificateManager:
|
||||||
return "/projects/cert-management/$projectId/subscribers";
|
return "/projects/cert-management/$projectId/subscribers";
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ import {
|
|||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
UpdatePitVersionLimitDTO,
|
UpdatePitVersionLimitDTO,
|
||||||
UpdateProjectDTO,
|
UpdateProjectDTO,
|
||||||
Workspace
|
Workspace,
|
||||||
|
WorkspaceEnv
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const fetchWorkspaceById = async (workspaceId: string) => {
|
export const fetchWorkspaceById = async (workspaceId: string) => {
|
||||||
@@ -396,12 +397,16 @@ export const useDeleteWorkspace = () => {
|
|||||||
export const useCreateWsEnvironment = () => {
|
export const useCreateWsEnvironment = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<object, object, CreateEnvironmentDTO>({
|
return useMutation<WorkspaceEnv, WorkspaceEnv, CreateEnvironmentDTO>({
|
||||||
mutationFn: ({ workspaceId, name, slug }) => {
|
mutationFn: async ({ workspaceId, name, slug }) => {
|
||||||
return apiRequest.post(`/api/v1/workspace/${workspaceId}/environments`, {
|
const { data } = await apiRequest.post<{ environment: WorkspaceEnv }>(
|
||||||
name,
|
`/api/v1/workspace/${workspaceId}/environments`,
|
||||||
slug
|
{
|
||||||
});
|
name,
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data.environment;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
|
|||||||
+4
-1
@@ -36,7 +36,10 @@ export const AssumePrivilegeModeBanner = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
const url = getProjectHomePage(currentWorkspace.type);
|
const url = getProjectHomePage(
|
||||||
|
currentWorkspace.type,
|
||||||
|
currentWorkspace.environments
|
||||||
|
);
|
||||||
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const ProjectSelect = () => {
|
|||||||
<div className="-mr-2 flex w-full items-center gap-1">
|
<div className="-mr-2 flex w-full items-center gap-1">
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<Link
|
<Link
|
||||||
to={getProjectHomePage(currentWorkspace.type)}
|
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
|
||||||
params={{
|
params={{
|
||||||
projectId: currentWorkspace.id
|
projectId: currentWorkspace.id
|
||||||
}}
|
}}
|
||||||
@@ -158,7 +158,7 @@ export const ProjectSelect = () => {
|
|||||||
// to reproduce change this back to router.push and switch between two projects with different env count
|
// to reproduce change this back to router.push and switch between two projects with different env count
|
||||||
// look into this on dashboard revamp
|
// look into this on dashboard revamp
|
||||||
const url = linkOptions({
|
const url = linkOptions({
|
||||||
to: getProjectHomePage(workspace.type),
|
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,9 +71,16 @@ export const SecretManagerLayout = () => {
|
|||||||
<Menu>
|
<Menu>
|
||||||
<MenuGroup title="Resources">
|
<MenuGroup title="Resources">
|
||||||
<Link
|
<Link
|
||||||
to="/projects/secret-management/$projectId/overview"
|
to={
|
||||||
|
currentWorkspace.environments.length
|
||||||
|
? "/projects/secret-management/$projectId/secrets/$envSlug"
|
||||||
|
: "/projects/secret-management/$projectId/overview"
|
||||||
|
}
|
||||||
params={{
|
params={{
|
||||||
projectId: currentWorkspace.id
|
projectId: currentWorkspace.id,
|
||||||
|
...(currentWorkspace.environments.length
|
||||||
|
? { envSlug: currentWorkspace.environments[0]?.slug }
|
||||||
|
: {})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import {
|
|||||||
useRequestProjectAccess,
|
useRequestProjectAccess,
|
||||||
useSearchProjects
|
useSearchProjects
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
import { ProjectType, Workspace, WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onAddNewProject: () => void;
|
onAddNewProject: () => void;
|
||||||
@@ -144,13 +144,17 @@ export const AllProjectView = ({
|
|||||||
type: projectTypeFilter
|
type: projectTypeFilter
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAccessProject = async (type: ProjectType, projectId: string) => {
|
const handleAccessProject = async (
|
||||||
|
type: ProjectType,
|
||||||
|
projectId: string,
|
||||||
|
environments: WorkspaceEnv[]
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
await orgAdminAccessProject.mutateAsync({
|
await orgAdminAccessProject.mutateAsync({
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
await navigate({
|
await navigate({
|
||||||
to: getProjectHomePage(type),
|
to: getProjectHomePage(type, environments),
|
||||||
params: {
|
params: {
|
||||||
projectId
|
projectId
|
||||||
}
|
}
|
||||||
@@ -307,7 +311,7 @@ export const AllProjectView = ({
|
|||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
if (evt.key === "Enter" && workspace.isMember) {
|
if (evt.key === "Enter" && workspace.isMember) {
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(workspace.type),
|
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
@@ -317,7 +321,7 @@ export const AllProjectView = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (workspace.isMember) {
|
if (workspace.isMember) {
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(workspace.type),
|
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
@@ -363,7 +367,7 @@ export const AllProjectView = ({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleAccessProject(workspace.type, workspace.id);
|
handleAccessProject(workspace.type, workspace.id, workspace.environments);
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
orgAdminAccessProject.variables?.projectId === workspace.id &&
|
orgAdminAccessProject.variables?.projectId === workspace.id &&
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export const MyProjectView = ({
|
|||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(workspace.type),
|
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ export const MyProjectView = ({
|
|||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(workspace.type),
|
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -136,7 +136,7 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
|
|||||||
text: "User privilege assumption has started"
|
text: "User privilege assumption has started"
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = getProjectHomePage(currentWorkspace.type);
|
const url = getProjectHomePage(currentWorkspace.type, currentWorkspace.environments);
|
||||||
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const Page = () => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
text: "Identity privilege assumption has started"
|
text: "Identity privilege assumption has started"
|
||||||
});
|
});
|
||||||
const url = getProjectHomePage(currentWorkspace.type);
|
const url = getProjectHomePage(currentWorkspace.type, currentWorkspace.environments);
|
||||||
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const Page = () => {
|
|||||||
text: "User privilege assumption has started"
|
text: "User privilege assumption has started"
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = getProjectHomePage(currentWorkspace.type);
|
const url = getProjectHomePage(currentWorkspace.type, currentWorkspace.environments);
|
||||||
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,7 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
|
|||||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||||
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
||||||
import { SecretV3RawSanitized } from "@app/hooks/api/types";
|
import { SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||||
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
|
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
|
||||||
import { useResizableColWidth } from "@app/hooks/useResizableColWidth";
|
import { useResizableColWidth } from "@app/hooks/useResizableColWidth";
|
||||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||||
@@ -64,6 +65,7 @@ import { ActionBar } from "./components/ActionBar";
|
|||||||
import { CommitForm } from "./components/CommitForm";
|
import { CommitForm } from "./components/CommitForm";
|
||||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||||
import { DynamicSecretListView } from "./components/DynamicSecretListView";
|
import { DynamicSecretListView } from "./components/DynamicSecretListView";
|
||||||
|
import { EnvironmentTabs } from "./components/EnvironmentTabs";
|
||||||
import { FolderListView } from "./components/FolderListView";
|
import { FolderListView } from "./components/FolderListView";
|
||||||
import { PitDrawer } from "./components/PitDrawer";
|
import { PitDrawer } from "./components/PitDrawer";
|
||||||
import { SecretDropzone } from "./components/SecretDropzone";
|
import { SecretDropzone } from "./components/SecretDropzone";
|
||||||
@@ -710,12 +712,18 @@ const Page = () => {
|
|||||||
|
|
||||||
const mergedSecrets = getMergedSecretsWithPending();
|
const mergedSecrets = getMergedSecretsWithPending();
|
||||||
const mergedFolders = getMergedFoldersWithPending();
|
const mergedFolders = getMergedFoldersWithPending();
|
||||||
|
|
||||||
|
if (!(currentWorkspace?.version === ProjectVersion.V3))
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
|
<SecretV2MigrationSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={
|
title="Secrets Management"
|
||||||
currentWorkspace.environments.find((env) => env.slug === environment)?.name ?? environment
|
|
||||||
}
|
|
||||||
description={
|
description={
|
||||||
<p className="text-md text-bunker-300">
|
<p className="text-md text-bunker-300">
|
||||||
Inject your secrets using
|
Inject your secrets using
|
||||||
@@ -759,6 +767,7 @@ const Page = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SecretV2MigrationSection />
|
<SecretV2MigrationSection />
|
||||||
|
<EnvironmentTabs secretPath={secretPath} />
|
||||||
{!isRollbackMode ? (
|
{!isRollbackMode ? (
|
||||||
<>
|
<>
|
||||||
<ActionBar
|
<ActionBar
|
||||||
|
|||||||
+589
@@ -0,0 +1,589 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { MultiValue } from "react-select";
|
||||||
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faCheckCircle,
|
||||||
|
faFilter,
|
||||||
|
faFingerprint,
|
||||||
|
faFolder,
|
||||||
|
faKey,
|
||||||
|
faRotate,
|
||||||
|
faSearch,
|
||||||
|
faWarning
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
FilterableSelect,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Lottie,
|
||||||
|
Pagination,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { useDebounce, usePagination, useResetPageHelper } from "@app/hooks";
|
||||||
|
import { useGetImportedSecretsAllEnvs } from "@app/hooks/api";
|
||||||
|
import { useGetProjectSecretsOverview } from "@app/hooks/api/dashboard";
|
||||||
|
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||||
|
import { useResizableColWidth } from "@app/hooks/useResizableColWidth";
|
||||||
|
import {
|
||||||
|
useDynamicSecretOverview,
|
||||||
|
useFolderOverview,
|
||||||
|
useSecretOverview,
|
||||||
|
useSecretRotationOverview
|
||||||
|
} from "@app/hooks/utils";
|
||||||
|
import { SecretTableResourceCount } from "@app/pages/secret-manager/OverviewPage/components/SecretTableResourceCount";
|
||||||
|
|
||||||
|
import { DynamicSecretRow } from "./components/DynamicSecretRow";
|
||||||
|
import { FolderRow } from "./components/FolderRow";
|
||||||
|
import { SecretRotationRow } from "./components/SecretRotationRow";
|
||||||
|
import { SecretNoAccessRow, SecretRow } from "./components/SecretRow";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum RowType {
|
||||||
|
Folder = "folder",
|
||||||
|
DynamicSecret = "dynamic",
|
||||||
|
Secret = "secret",
|
||||||
|
SecretRotation = "rotation"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter = {
|
||||||
|
[key in RowType]: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_FILTER_STATE = {
|
||||||
|
[RowType.Folder]: false,
|
||||||
|
[RowType.DynamicSecret]: false,
|
||||||
|
[RowType.Secret]: false,
|
||||||
|
[RowType.SecretRotation]: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const TABLE_WIDTH_OFFSET = 17;
|
||||||
|
const COL_WIDTH_OFFSET = 220;
|
||||||
|
|
||||||
|
export const CompareEnvironments = ({ secretPath }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const compareEnvironmentsKey = `compare-environments-${currentWorkspace.id}`;
|
||||||
|
|
||||||
|
const [selectedEnvironments, setSelectedEnvironments] = useState<WorkspaceEnv[]>(() => {
|
||||||
|
try {
|
||||||
|
const storedEnvironments = JSON.parse(localStorage.getItem(compareEnvironmentsKey) ?? "[]");
|
||||||
|
|
||||||
|
if (Array.isArray(storedEnvironments) && storedEnvironments.length > 0) {
|
||||||
|
const potentialEnvs: string[] = [];
|
||||||
|
storedEnvironments.forEach((env) => {
|
||||||
|
if (typeof env === "string") {
|
||||||
|
potentialEnvs.push(env);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return currentWorkspace.environments.filter((env) => potentialEnvs.includes(env.id));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// do nothing and proceed
|
||||||
|
}
|
||||||
|
return currentWorkspace.environments.slice(0, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE);
|
||||||
|
|
||||||
|
const {
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
orderDirection,
|
||||||
|
setOrderDirection,
|
||||||
|
setPage,
|
||||||
|
perPage,
|
||||||
|
page,
|
||||||
|
setPerPage,
|
||||||
|
orderBy
|
||||||
|
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name, {
|
||||||
|
initPerPage: getUserTablePreference("secretCompareTable", PreferenceKey.PerPage, 50)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("secretCompareTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const workspaceId = currentWorkspace.id;
|
||||||
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
|
const [debouncedSearchFilter] = useDebounce(searchFilter);
|
||||||
|
const [debouncedSelectedEnvironments] = useDebounce(selectedEnvironments);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
compareEnvironmentsKey,
|
||||||
|
JSON.stringify(selectedEnvironments.map((env) => env.id))
|
||||||
|
);
|
||||||
|
}, [debouncedSelectedEnvironments]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
secretImports,
|
||||||
|
isImportedSecretPresentInEnv,
|
||||||
|
getImportedSecretByKey,
|
||||||
|
getEnvImportedSecretKeyCount
|
||||||
|
} = useGetImportedSecretsAllEnvs({
|
||||||
|
projectId: workspaceId,
|
||||||
|
path: secretPath,
|
||||||
|
environments: (currentWorkspace.environments || []).map(({ slug }) => slug)
|
||||||
|
});
|
||||||
|
|
||||||
|
const compareEnvironments = selectedEnvironments.length
|
||||||
|
? selectedEnvironments
|
||||||
|
: currentWorkspace.environments;
|
||||||
|
|
||||||
|
const isFilteredByResources = Object.values(filter).some(Boolean);
|
||||||
|
const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
|
||||||
|
{
|
||||||
|
projectId: workspaceId,
|
||||||
|
environments: compareEnvironments.map((env) => env.slug),
|
||||||
|
secretPath,
|
||||||
|
orderDirection,
|
||||||
|
orderBy,
|
||||||
|
includeFolders: isFilteredByResources ? filter.folder : true,
|
||||||
|
includeDynamicSecrets: isFilteredByResources ? filter.dynamic : true,
|
||||||
|
includeSecrets: isFilteredByResources ? filter.secret : true,
|
||||||
|
includeImports: true,
|
||||||
|
includeSecretRotations: isFilteredByResources ? filter.rotation : true,
|
||||||
|
search: debouncedSearchFilter,
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
},
|
||||||
|
{ enabled: Boolean(compareEnvironments.length) }
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
secrets,
|
||||||
|
folders,
|
||||||
|
dynamicSecrets,
|
||||||
|
secretRotations,
|
||||||
|
totalFolderCount,
|
||||||
|
totalSecretCount,
|
||||||
|
totalDynamicSecretCount,
|
||||||
|
totalSecretRotationCount,
|
||||||
|
totalImportCount,
|
||||||
|
totalCount = 0,
|
||||||
|
totalUniqueFoldersInPage,
|
||||||
|
totalUniqueSecretsInPage,
|
||||||
|
totalUniqueSecretImportsInPage,
|
||||||
|
totalUniqueDynamicSecretsInPage,
|
||||||
|
totalUniqueSecretRotationsInPage
|
||||||
|
} = overview ?? {};
|
||||||
|
|
||||||
|
const secretImportsShaped = secretImports
|
||||||
|
?.flatMap(({ data }) => data)
|
||||||
|
.filter(Boolean)
|
||||||
|
.flatMap((item) => item?.secrets || []);
|
||||||
|
|
||||||
|
const handleIsImportedSecretPresentInEnv = (envSlug: string, secretName: string) => {
|
||||||
|
if (secrets?.some((s) => s.key === secretName && s.env === envSlug)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (secretImportsShaped.some((s) => s.key === secretName && s.sourceEnv === envSlug)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isImportedSecretPresentInEnv(envSlug, secretName);
|
||||||
|
};
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
|
const { folderNamesAndDescriptions, isFolderPresentInEnv } = useFolderOverview(folders);
|
||||||
|
|
||||||
|
const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
|
||||||
|
useDynamicSecretOverview(dynamicSecrets);
|
||||||
|
|
||||||
|
const { secretRotationNames, isSecretRotationPresentInEnv, getSecretRotationByName } =
|
||||||
|
useSecretRotationOverview(secretRotations);
|
||||||
|
|
||||||
|
const { secKeys, getEnvSecretKeyCount } = useSecretOverview(
|
||||||
|
secrets?.concat(secretImportsShaped) || []
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSecretByKey = useCallback(
|
||||||
|
(env: string, key: string) => {
|
||||||
|
const sec = secrets?.find((s) => s.env === env && s.key === key);
|
||||||
|
return sec;
|
||||||
|
},
|
||||||
|
[secrets]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [tableWidth, setTableWidth] = useState(0);
|
||||||
|
|
||||||
|
const tableRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { handleMouseDown, isResizing, colWidth } = useResizableColWidth({
|
||||||
|
initialWidth: 320,
|
||||||
|
minWidth: 160,
|
||||||
|
maxWidth: tableRef.current
|
||||||
|
? tableRef.current.clientWidth - COL_WIDTH_OFFSET // ensure value column can't collapse completely
|
||||||
|
: 800
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggleRowType = useCallback(
|
||||||
|
(rowType: RowType) =>
|
||||||
|
setFilter((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[rowType]: !state[rowType]
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isTableEmpty = totalCount === 0;
|
||||||
|
|
||||||
|
const isTableFiltered = isFilteredByResources;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const entry of entries) {
|
||||||
|
setTableWidth(entry.contentRect.width - TABLE_WIDTH_OFFSET);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tableRef.current) {
|
||||||
|
resizeObserver.observe(tableRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// scott: this is reverse to fix z-indexing bug of dropdown with sticky table cols; couldn't resolve with flex-col
|
||||||
|
<div className="flex flex-1 flex-col-reverse overflow-hidden">
|
||||||
|
{!isOverviewLoading && totalCount > 0 && (
|
||||||
|
<Pagination
|
||||||
|
startAdornment={
|
||||||
|
<SecretTableResourceCount
|
||||||
|
dynamicSecretCount={totalDynamicSecretCount}
|
||||||
|
secretCount={totalSecretCount}
|
||||||
|
folderCount={totalFolderCount}
|
||||||
|
importCount={totalImportCount}
|
||||||
|
secretRotationCount={totalSecretRotationCount}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className="rounded-b-lg border border-solid border-mineshaft-500 bg-mineshaft-700"
|
||||||
|
count={totalCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={handlePerPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div ref={tableRef} className="thin-scrollbar flex flex-1 flex-col overflow-y-auto">
|
||||||
|
<TableContainer
|
||||||
|
className={twMerge(
|
||||||
|
"mt-4 flex flex-1 flex-col border-mineshaft-500 bg-mineshaft-700",
|
||||||
|
!isTableEmpty && "rounded-b-none"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
|
{isOverviewLoading ? (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center">
|
||||||
|
<Lottie
|
||||||
|
isAutoPlay
|
||||||
|
icon="infisical_loading"
|
||||||
|
className="h-10 place-self-center self-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : isTableEmpty ? (
|
||||||
|
<EmptyState
|
||||||
|
titleClassName="text-base"
|
||||||
|
className="m-auto bg-mineshaft-700 text-lg"
|
||||||
|
title={
|
||||||
|
isTableFiltered
|
||||||
|
? "No secrets to compare with current filters"
|
||||||
|
: "No secrets to compare"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Table className="border-collapse bg-mineshaft-700">
|
||||||
|
<THead className="sticky top-0 z-20">
|
||||||
|
<Tr className="sticky top-0 z-20">
|
||||||
|
<Th className="sticky left-0 z-10 border-none p-0" style={{ width: colWidth }}>
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
tabIndex={-1}
|
||||||
|
role="button"
|
||||||
|
className={`absolute -right-[0.02rem] z-40 h-full w-0.5 cursor-ew-resize hover:bg-blue-400/20 ${
|
||||||
|
isResizing ? "bg-blue-400/75" : "bg-transparent"
|
||||||
|
}`}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
/>
|
||||||
|
<div className="pointer-events-none absolute -right-[0.02rem] top-[0.67rem] z-30">
|
||||||
|
<div className="h-5 w-0.5 rounded-[1.5px] bg-gray-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full items-center border-b-2 border-r border-mineshaft-500 bg-mineshaft-700 bg-clip-padding p-0 px-4 py-2.5">
|
||||||
|
Name
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className="ml-2"
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() =>
|
||||||
|
setOrderDirection((prev) =>
|
||||||
|
prev === OrderByDirection.ASC
|
||||||
|
? OrderByDirection.DESC
|
||||||
|
: OrderByDirection.ASC
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={orderDirection === "asc" ? faArrowDown : faArrowUp}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
{compareEnvironments?.map(({ name, slug }, index) => {
|
||||||
|
const envSecKeyCount = getEnvSecretKeyCount(slug);
|
||||||
|
const importedSecKeyCount = getEnvImportedSecretKeyCount(slug);
|
||||||
|
const missingKeyCount = secKeys.length - envSecKeyCount - importedSecKeyCount;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Th
|
||||||
|
className="whitespace-nowrap border-none p-0 text-center"
|
||||||
|
key={`environment-${slug}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-full w-full items-center justify-center gap-x-2 border-b-2 border-mineshaft-500 bg-mineshaft-700 p-0 px-4 py-3 text-center",
|
||||||
|
index < compareEnvironments.length - 1 && "border-r"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
{missingKeyCount > 0 && (
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-none lowercase"
|
||||||
|
content={`${missingKeyCount} secrets missing\n compared to other environments`}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant="primary"
|
||||||
|
className="-mt-[0.05rem] flex h-4 items-center gap-x-1 pt-[0.1rem] font-normal leading-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faWarning} className="-mt-[0.1rem] w-2.5" />
|
||||||
|
{missingKeyCount}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{folderNamesAndDescriptions.map(({ name: folderName }, index) => (
|
||||||
|
<FolderRow
|
||||||
|
folderName={folderName}
|
||||||
|
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||||
|
environments={compareEnvironments}
|
||||||
|
key={`overview-${folderName}-${index + 1}`}
|
||||||
|
colWidth={colWidth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{dynamicSecretNames.map((dynamicSecretName, index) => (
|
||||||
|
<DynamicSecretRow
|
||||||
|
dynamicSecretName={dynamicSecretName}
|
||||||
|
isDynamicSecretInEnv={isDynamicSecretPresentInEnv}
|
||||||
|
environments={compareEnvironments}
|
||||||
|
key={`overview-${dynamicSecretName}-${index + 1}`}
|
||||||
|
colWidth={colWidth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{secretRotationNames.map((secretRotationName, index) => (
|
||||||
|
<SecretRotationRow
|
||||||
|
secretRotationName={secretRotationName}
|
||||||
|
isSecretRotationInEnv={isSecretRotationPresentInEnv}
|
||||||
|
environments={compareEnvironments}
|
||||||
|
getSecretRotationByName={getSecretRotationByName}
|
||||||
|
key={`overview-${secretRotationName}-${index + 1}`}
|
||||||
|
colWidth={colWidth}
|
||||||
|
tableWidth={tableWidth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{secKeys.map((key, index) => (
|
||||||
|
<SecretRow
|
||||||
|
colWidth={colWidth}
|
||||||
|
secretPath={secretPath}
|
||||||
|
getImportedSecretByKey={getImportedSecretByKey}
|
||||||
|
isImportedSecretPresentInEnv={handleIsImportedSecretPresentInEnv}
|
||||||
|
key={`overview-${key}-${index + 1}`}
|
||||||
|
environments={compareEnvironments}
|
||||||
|
secretKey={key}
|
||||||
|
getSecretByKey={getSecretByKey}
|
||||||
|
tableWidth={tableWidth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<SecretNoAccessRow
|
||||||
|
colWidth={colWidth}
|
||||||
|
environments={compareEnvironments}
|
||||||
|
count={Math.max(
|
||||||
|
(page * perPage > totalCount ? totalCount % perPage : perPage) -
|
||||||
|
(totalUniqueFoldersInPage || 0) -
|
||||||
|
(totalUniqueDynamicSecretsInPage || 0) -
|
||||||
|
(totalUniqueSecretsInPage || 0) -
|
||||||
|
(totalUniqueSecretImportsInPage || 0) -
|
||||||
|
(totalUniqueSecretRotationsInPage || 0),
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex flex-row items-center justify-center space-x-2">
|
||||||
|
<Input
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
className="h-full flex-1"
|
||||||
|
placeholder="Search by resource name..."
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||||
|
containerClassName="h-10"
|
||||||
|
/>
|
||||||
|
{isTableFiltered && (
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setFilter(DEFAULT_FILTER_STATE);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{compareEnvironments.length > 0 && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline_bg"
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-[2.5rem]",
|
||||||
|
isTableFiltered && "border-primary/40 bg-primary/10"
|
||||||
|
)}
|
||||||
|
leftIcon={
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faFilter}
|
||||||
|
className={isTableFiltered ? "text-primary/80" : undefined}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Filters
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
|
||||||
|
align="end"
|
||||||
|
sideOffset={2}
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>Filter by Resource</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleRowType(RowType.Folder);
|
||||||
|
}}
|
||||||
|
icon={filter[RowType.Folder] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faFolder} className="text-yellow-700" />
|
||||||
|
<span>Folders</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleRowType(RowType.DynamicSecret);
|
||||||
|
}}
|
||||||
|
icon={filter[RowType.DynamicSecret] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faFingerprint} className="text-yellow-700" />
|
||||||
|
<span>Dynamic Secrets</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleRowType(RowType.SecretRotation);
|
||||||
|
}}
|
||||||
|
icon={filter[RowType.SecretRotation] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faRotate} className="text-mineshaft-400" />
|
||||||
|
<span>Secret Rotations</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleRowType(RowType.Secret);
|
||||||
|
}}
|
||||||
|
icon={filter[RowType.Secret] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faKey} className="text-bunker-300" />
|
||||||
|
<span>Secrets</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="!z-[99999999]">
|
||||||
|
<FormLabel label="Select Environments to Compare" />
|
||||||
|
<FilterableSelect
|
||||||
|
value={selectedEnvironments}
|
||||||
|
onChange={(value) => {
|
||||||
|
const selected = value as MultiValue<WorkspaceEnv>;
|
||||||
|
|
||||||
|
setSelectedEnvironments((selected as WorkspaceEnv[]) ?? []);
|
||||||
|
}}
|
||||||
|
placeholder="Leave blank to compare all environments"
|
||||||
|
options={currentWorkspace.environments}
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { faFingerprint } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { Tr } from "@app/components/v2";
|
||||||
|
|
||||||
|
import { EnvironmentStatusCell, ResourceNameCell } from "../shared";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dynamicSecretName: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
isDynamicSecretInEnv: (name: string, env: string) => boolean;
|
||||||
|
colWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DynamicSecretRow = ({
|
||||||
|
dynamicSecretName,
|
||||||
|
environments = [],
|
||||||
|
isDynamicSecretInEnv,
|
||||||
|
colWidth
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Tr isHoverable className="group border-mineshaft-500">
|
||||||
|
<ResourceNameCell
|
||||||
|
label={dynamicSecretName}
|
||||||
|
icon={faFingerprint}
|
||||||
|
colWidth={colWidth}
|
||||||
|
iconClassName="text-yellow-700"
|
||||||
|
/>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const isPresent = isDynamicSecretInEnv(dynamicSecretName, slug);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnvironmentStatusCell
|
||||||
|
isLast={i === environments.length - 1}
|
||||||
|
status={isPresent ? "present" : "missing"}
|
||||||
|
key={`dynamic-secret-${dynamicSecretName}-${i + 1}-value`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { DynamicSecretRow } from "./DynamicSecretRow";
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { Tr } from "@app/components/v2";
|
||||||
|
|
||||||
|
import { EnvironmentStatusCell, ResourceNameCell } from "../shared";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
folderName: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
isFolderPresentInEnv: (name: string, env: string) => boolean;
|
||||||
|
colWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FolderRow = ({
|
||||||
|
folderName,
|
||||||
|
environments = [],
|
||||||
|
isFolderPresentInEnv,
|
||||||
|
colWidth
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Tr isHoverable className="group border-mineshaft-500">
|
||||||
|
<ResourceNameCell
|
||||||
|
label={folderName}
|
||||||
|
icon={faFolder}
|
||||||
|
iconClassName="text-yellow-700"
|
||||||
|
colWidth={colWidth}
|
||||||
|
/>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const isPresent = isFolderPresentInEnv(folderName, slug);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnvironmentStatusCell
|
||||||
|
isLast={i === environments.length - 1}
|
||||||
|
status={isPresent ? "present" : "missing"}
|
||||||
|
key={`folder-${slug}-${i + 1}-value`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { FolderRow } from "./FolderRow";
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
import { faEye, faEyeSlash, faInfoCircle, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Button, TableContainer, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
||||||
|
import { Blur } from "@app/components/v2/Blur";
|
||||||
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
|
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||||
|
|
||||||
|
import { EnvironmentStatusCell, ResourceNameCell } from "../shared";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretRotationName: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
isSecretRotationInEnv: (name: string, env: string) => boolean;
|
||||||
|
getSecretRotationByName: (slug: string, name: string) => TSecretRotationV2 | undefined;
|
||||||
|
colWidth: number;
|
||||||
|
tableWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretRotationRow = ({
|
||||||
|
secretRotationName,
|
||||||
|
environments = [],
|
||||||
|
isSecretRotationInEnv,
|
||||||
|
colWidth,
|
||||||
|
getSecretRotationByName,
|
||||||
|
tableWidth
|
||||||
|
}: Props) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useToggle(false);
|
||||||
|
const [isSecretVisible, setIsSecretVisible] = useToggle();
|
||||||
|
|
||||||
|
const totalCols = environments.length + 1; // secret key row
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr isHoverable isSelectable onClick={setIsExpanded.toggle} className="group">
|
||||||
|
<ResourceNameCell
|
||||||
|
label={secretRotationName}
|
||||||
|
icon={faRotate}
|
||||||
|
colWidth={colWidth}
|
||||||
|
iconClassName="text-mineshaft-400"
|
||||||
|
isRowExpanded={isExpanded}
|
||||||
|
/>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const isPresent = isSecretRotationInEnv(secretRotationName, slug);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnvironmentStatusCell
|
||||||
|
isLast={i === environments.length - 1}
|
||||||
|
status={isPresent ? "present" : "missing"}
|
||||||
|
key={`secret-rotation-${slug}-${i + 1}-value`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
{isExpanded &&
|
||||||
|
environments.map(({ name: envName, slug }) => {
|
||||||
|
const secretRotation = getSecretRotationByName(slug, secretRotationName);
|
||||||
|
|
||||||
|
if (!secretRotation) return null;
|
||||||
|
|
||||||
|
const { type, secrets, description } = secretRotation;
|
||||||
|
|
||||||
|
const { name: rotationType, image } = SECRET_ROTATION_MAP[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`secret-rotation-${slug}-${secretRotationName}`} className="border-b-0">
|
||||||
|
<Td
|
||||||
|
colSpan={totalCols}
|
||||||
|
style={{ minWidth: tableWidth, maxWidth: tableWidth }}
|
||||||
|
className="sticky left-0 bg-mineshaft-800 bg-clip-padding px-0 py-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ minWidth: tableWidth, maxWidth: tableWidth }}
|
||||||
|
className="sticky left-0 bg-mineshaft-800 bg-clip-padding px-0 py-0"
|
||||||
|
>
|
||||||
|
<div className="flex !h-[40px] items-center justify-between gap-x-2 px-4">
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex w-full flex-wrap items-center gap-x-2.5">
|
||||||
|
<span>{envName}</span>
|
||||||
|
<Tag className="flex items-center gap-1 px-1.5 py-0 text-xs normal-case">
|
||||||
|
<img
|
||||||
|
src={`/images/integrations/${image}`}
|
||||||
|
className="w-[11px]"
|
||||||
|
alt={`${rotationType} logo`}
|
||||||
|
/>
|
||||||
|
{rotationType}
|
||||||
|
</Tag>
|
||||||
|
{description && (
|
||||||
|
<Tooltip content={description}>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />}
|
||||||
|
onClick={() => setIsSecretVisible.toggle()}
|
||||||
|
>
|
||||||
|
{isSecretVisible ? "Hide Values" : "Reveal Values"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<TableContainer className="rounded-none border-0">
|
||||||
|
<table className="secret-table w-full border-b-0">
|
||||||
|
<tbody className="!last:border-b-0 w-full border-t-2 border-mineshaft-600">
|
||||||
|
{secrets.map((secret, index) => {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-sm"
|
||||||
|
content={
|
||||||
|
secret
|
||||||
|
? undefined
|
||||||
|
: "You do not have permission to view this secret."
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||||
|
>
|
||||||
|
<tr className="hover:bg-mineshaft-700/70">
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
width: colWidth
|
||||||
|
}}
|
||||||
|
className="!h-[1px] border-none !p-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex h-full flex-1 items-center border-r border-mineshaft-500 px-4 py-1"
|
||||||
|
style={{
|
||||||
|
width: colWidth
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={twMerge(!secret && "blur", "truncate")}>
|
||||||
|
{secret?.key ?? "********"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="!h-[40px] !px-4">
|
||||||
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
|
{!secret ? (
|
||||||
|
<div className="h-full pl-4 blur">********</div>
|
||||||
|
) : secret.secretValueHidden ? (
|
||||||
|
<Blur
|
||||||
|
className="py-0"
|
||||||
|
tooltipText="You do not have permission to read the value of this secret."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<InfisicalSecretInput
|
||||||
|
isReadOnly
|
||||||
|
value={secret.value}
|
||||||
|
isVisible={isSecretVisible}
|
||||||
|
secretPath={secretRotation.folder.path}
|
||||||
|
environment={secretRotation.environment.slug}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { SecretRotationRow } from "./SecretRotationRow";
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
import { faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Tooltip } from "@app/components/v2";
|
||||||
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultValue?: string | null;
|
||||||
|
isOverride?: boolean;
|
||||||
|
isVisible?: boolean;
|
||||||
|
isImportedSecret: boolean;
|
||||||
|
environment: string;
|
||||||
|
secretValueHidden: boolean;
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EnvironmentSecretRow = ({
|
||||||
|
defaultValue,
|
||||||
|
isOverride,
|
||||||
|
isImportedSecret,
|
||||||
|
secretValueHidden,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
isVisible
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="group flex w-full cursor-text items-center space-x-2">
|
||||||
|
{secretValueHidden && !isOverride && (
|
||||||
|
<Tooltip content="You do not have access to view the current value">
|
||||||
|
<FontAwesomeIcon className="pl-2" size="sm" icon={faEyeSlash} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 pl-3 pr-2">
|
||||||
|
<InfisicalSecretInput
|
||||||
|
onChange={() => {}}
|
||||||
|
isReadOnly
|
||||||
|
value={defaultValue as string}
|
||||||
|
key="secret-input"
|
||||||
|
isVisible={isVisible && !secretValueHidden}
|
||||||
|
secretPath={secretPath}
|
||||||
|
environment={environment}
|
||||||
|
isImport={isImportedSecret}
|
||||||
|
defaultValue={secretValueHidden ? "" : undefined}
|
||||||
|
canEditButNotView={secretValueHidden && !isOverride}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { Tr } from "@app/components/v2";
|
||||||
|
import { Blur } from "@app/components/v2/Blur";
|
||||||
|
|
||||||
|
import { EnvironmentStatusCell, ResourceNameCell } from "../shared";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
count: number;
|
||||||
|
colWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretNoAccessRow = ({ environments = [], count, colWidth }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Array.from(Array(count)).map((_, j) => (
|
||||||
|
<Tr
|
||||||
|
key={`no-access-secret-${j + 1}`}
|
||||||
|
isHoverable
|
||||||
|
isSelectable
|
||||||
|
className="group border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<ResourceNameCell
|
||||||
|
label={<Blur />}
|
||||||
|
iconClassName="text-bunker-400"
|
||||||
|
icon={faLock}
|
||||||
|
colWidth={colWidth}
|
||||||
|
tooltipContent="You do not have permission to view this secret"
|
||||||
|
/>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
return (
|
||||||
|
<EnvironmentStatusCell
|
||||||
|
isLast={i === environments.length - 1}
|
||||||
|
status="no-access"
|
||||||
|
key={`no-access-sec--${slug}-${i + 1}-value`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
import { subject } from "@casl/ability";
|
||||||
|
import {
|
||||||
|
faCodeBranch,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faFileImport,
|
||||||
|
faKey,
|
||||||
|
faRotate
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button, TableContainer, Td, Tooltip, Tr } from "@app/components/v2";
|
||||||
|
import { useProjectPermission } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||||
|
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||||
|
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||||
|
|
||||||
|
import { EnvironmentStatus, EnvironmentStatusCell, ResourceNameCell } from "../shared";
|
||||||
|
import { EnvironmentSecretRow } from "./EnvironmentSecretRow";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretKey: string;
|
||||||
|
secretPath: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
|
||||||
|
isImportedSecretPresentInEnv: (env: string, secretName: string) => boolean;
|
||||||
|
getImportedSecretByKey: (
|
||||||
|
env: string,
|
||||||
|
secretName: string
|
||||||
|
) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined;
|
||||||
|
colWidth: number;
|
||||||
|
tableWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretRow = ({
|
||||||
|
secretKey,
|
||||||
|
environments = [],
|
||||||
|
secretPath,
|
||||||
|
getSecretByKey,
|
||||||
|
isImportedSecretPresentInEnv,
|
||||||
|
getImportedSecretByKey,
|
||||||
|
colWidth,
|
||||||
|
tableWidth
|
||||||
|
}: Props) => {
|
||||||
|
const [isFormExpanded, setIsFormExpanded] = useToggle();
|
||||||
|
const totalCols = environments.length + 1; // secret key row
|
||||||
|
const [isSecretVisible, setIsSecretVisible] = useToggle();
|
||||||
|
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
|
const getDefaultValue = (
|
||||||
|
secret: SecretV3RawSanitized | undefined,
|
||||||
|
importedSecret: { secret?: SecretV3RawSanitized } | undefined
|
||||||
|
) => {
|
||||||
|
const canEditSecretValue = permission.can(
|
||||||
|
ProjectPermissionSecretActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: secret?.env || "",
|
||||||
|
secretPath: secret?.path || "",
|
||||||
|
secretName: secret?.key || "",
|
||||||
|
secretTags: ["*"]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (secret?.secretValueHidden && !secret?.valueOverride) {
|
||||||
|
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||||
|
}
|
||||||
|
return secret?.valueOverride || secret?.value || importedSecret?.secret?.value || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr
|
||||||
|
isHoverable
|
||||||
|
isSelectable
|
||||||
|
onClick={() => setIsFormExpanded.toggle()}
|
||||||
|
className="group border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<ResourceNameCell
|
||||||
|
colWidth={colWidth}
|
||||||
|
label={secretKey}
|
||||||
|
icon={faKey}
|
||||||
|
iconClassName="text-bunker-300"
|
||||||
|
isRowExpanded={isFormExpanded}
|
||||||
|
/>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const secret = getSecretByKey(slug, secretKey);
|
||||||
|
|
||||||
|
const isSecretImported = isImportedSecretPresentInEnv(slug, secretKey);
|
||||||
|
|
||||||
|
const isSecretPresent = Boolean(secret);
|
||||||
|
const isSecretEmpty = secret?.value === "";
|
||||||
|
|
||||||
|
let status: EnvironmentStatus;
|
||||||
|
|
||||||
|
if (isSecretEmpty) {
|
||||||
|
status = "empty";
|
||||||
|
} else if (isSecretPresent) {
|
||||||
|
status = "present";
|
||||||
|
} else if (isSecretImported) {
|
||||||
|
status = "imported";
|
||||||
|
} else {
|
||||||
|
status = "missing";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnvironmentStatusCell
|
||||||
|
isLast={i === environments.length - 1}
|
||||||
|
key={`sec-overview-${slug}-${i + 1}-value`}
|
||||||
|
status={status}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
{isFormExpanded && (
|
||||||
|
<Tr className="border-b-0">
|
||||||
|
<Td
|
||||||
|
colSpan={totalCols}
|
||||||
|
style={{ minWidth: tableWidth, maxWidth: tableWidth }}
|
||||||
|
className="sticky left-0 bg-mineshaft-800 bg-clip-padding px-0 py-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ minWidth: tableWidth, maxWidth: tableWidth }}
|
||||||
|
className="sticky left-0 bg-mineshaft-800 bg-clip-padding px-0 py-0"
|
||||||
|
>
|
||||||
|
<TableContainer className="rounded-none border-0">
|
||||||
|
<table className="secret-table">
|
||||||
|
<thead>
|
||||||
|
<tr className="h-10 border-b-2 border-mineshaft-600">
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
width: colWidth
|
||||||
|
}}
|
||||||
|
className="min-table-row"
|
||||||
|
>
|
||||||
|
<span className="truncate">Environment</span>
|
||||||
|
</th>
|
||||||
|
<th style={{ padding: "0.5rem 1rem" }} className="border-none">
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
<div className="absolute right-3 top-[1px] ml-auto mr-1 mt-1 w-min">
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />}
|
||||||
|
onClick={() => setIsSecretVisible.toggle()}
|
||||||
|
>
|
||||||
|
{isSecretVisible ? "Hide Values" : "Reveal Values"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="border-t-2 border-mineshaft-600">
|
||||||
|
{environments.map(({ name, slug }) => {
|
||||||
|
const secret = getSecretByKey(slug, secretKey);
|
||||||
|
|
||||||
|
const isImportedSecret = isImportedSecretPresentInEnv(slug, secretKey);
|
||||||
|
const importedSecret = getImportedSecretByKey(slug, secretKey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={`secret-expanded-${slug}-${secretKey}`}
|
||||||
|
className="h-full hover:bg-mineshaft-700/70"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="h-[1px] border-none !p-0"
|
||||||
|
style={{
|
||||||
|
width: colWidth
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
title={name}
|
||||||
|
style={{
|
||||||
|
width: colWidth
|
||||||
|
}}
|
||||||
|
className="flex h-full min-h-[40px] w-[8rem] items-center space-x-2 border-r border-mineshaft-500 px-4"
|
||||||
|
>
|
||||||
|
<span className="truncate">{name}</span>
|
||||||
|
{isImportedSecret && (
|
||||||
|
<Tooltip
|
||||||
|
content={`Imported secret from the '${importedSecret?.environmentInfo?.name}' environment`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFileImport} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{secret?.isRotatedSecret && (
|
||||||
|
<Tooltip content="Rotated Secret">
|
||||||
|
<FontAwesomeIcon icon={faRotate} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{secret?.valueOverride && (
|
||||||
|
<Tooltip content="Personal Override">
|
||||||
|
<FontAwesomeIcon icon={faCodeBranch} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="col-span-2 h-8 w-full">
|
||||||
|
<EnvironmentSecretRow
|
||||||
|
secretPath={secretPath}
|
||||||
|
isVisible={isSecretVisible}
|
||||||
|
secretValueHidden={secret?.secretValueHidden || false}
|
||||||
|
defaultValue={getDefaultValue(secret, importedSecret)}
|
||||||
|
isOverride={Boolean(secret?.valueOverride)}
|
||||||
|
isImportedSecret={isImportedSecret}
|
||||||
|
environment={slug}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export { SecretNoAccessRow } from "./SecretNoAccessRow";
|
||||||
|
export { SecretRow } from "./SecretRow";
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { faBan, faCheck, faFileImport, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Td, Tooltip } from "@app/components/v2";
|
||||||
|
|
||||||
|
export type EnvironmentStatus = "present" | "missing" | "empty" | "imported" | "no-access";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isLast: boolean;
|
||||||
|
status: EnvironmentStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EnvironmentStatusCell = ({ isLast, status }: Props) => {
|
||||||
|
let tooltipContent: string;
|
||||||
|
let icon: IconDefinition;
|
||||||
|
let iconClassName: string;
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "present":
|
||||||
|
tooltipContent = "Present in environment";
|
||||||
|
icon = faCheck;
|
||||||
|
iconClassName = "h-3 w-3";
|
||||||
|
break;
|
||||||
|
case "missing":
|
||||||
|
tooltipContent = "Missing from environment";
|
||||||
|
icon = faXmark;
|
||||||
|
iconClassName = "h-3.5 w-3.5";
|
||||||
|
break;
|
||||||
|
case "empty":
|
||||||
|
tooltipContent = "Empty value in environment";
|
||||||
|
icon = faCircle;
|
||||||
|
iconClassName = "h-3 w-3";
|
||||||
|
break;
|
||||||
|
case "imported":
|
||||||
|
tooltipContent = "Imported into environment";
|
||||||
|
icon = faFileImport;
|
||||||
|
iconClassName = "h-3 w-3";
|
||||||
|
break;
|
||||||
|
case "no-access":
|
||||||
|
tooltipContent = "You do not have permission to view this secret";
|
||||||
|
icon = faBan;
|
||||||
|
iconClassName = "h-3 w-3";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled environment status: ${status as string}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Td
|
||||||
|
className={twMerge(
|
||||||
|
"border-b border-mineshaft-500 bg-clip-padding p-0 group-hover:bg-mineshaft-600",
|
||||||
|
(status === "present" || status === "imported") && "text-green-600",
|
||||||
|
status === "empty" && "text-yellow",
|
||||||
|
status === "missing" && "text-red-600",
|
||||||
|
status === "no-access" && "text-bunker-400"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-10 items-center justify-center border-mineshaft-500 px-0 py-3",
|
||||||
|
!isLast && "border-r"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Tooltip center content={tooltipContent}>
|
||||||
|
<FontAwesomeIcon className={iconClassName} icon={icon} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
};
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Td, Tooltip } from "@app/components/v2";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isRowExpanded?: boolean;
|
||||||
|
label: ReactElement | string;
|
||||||
|
icon: IconDefinition;
|
||||||
|
iconClassName?: string;
|
||||||
|
colWidth: number;
|
||||||
|
tooltipContent?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ResourceNameCell = ({
|
||||||
|
isRowExpanded,
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
iconClassName,
|
||||||
|
colWidth,
|
||||||
|
tooltipContent
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Td
|
||||||
|
className="sticky left-0 z-10 border-b border-mineshaft-500 bg-mineshaft-700 bg-clip-padding p-0 group-hover:bg-mineshaft-600"
|
||||||
|
style={{
|
||||||
|
width: colWidth
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip content={tooltipContent} className="max-w-sm">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: colWidth
|
||||||
|
}}
|
||||||
|
className="flex h-10 items-center space-x-5 border-r border-mineshaft-600 px-4 py-2.5"
|
||||||
|
>
|
||||||
|
<div className="w-5 min-w-5">
|
||||||
|
<FontAwesomeIcon className={iconClassName} icon={isRowExpanded ? faAngleDown : icon} />
|
||||||
|
</div>
|
||||||
|
{typeof label === "string" ? <span className="truncate">{label}</span> : label}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./EnvironmentStatusCell";
|
||||||
|
export * from "./ResourceNameCell";
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./CompareEnvironments";
|
||||||
+242
@@ -0,0 +1,242 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faArrowRightArrowLeft, faEllipsisH, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
Tabs,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useSubscription,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { workspaceKeys } from "@app/hooks/api";
|
||||||
|
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||||
|
import { AddEnvironmentModal } from "@app/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal";
|
||||||
|
|
||||||
|
import { CompareEnvironments } from "../CompareEnvironments";
|
||||||
|
|
||||||
|
const COMPARE_ENVIRONMENT_TAB = "__COMPARE_ENVIRONMENT_TAB__";
|
||||||
|
const ADD_ENVIRONMENT_TAB = "__ADD_ENVIRONMENT_TAB__";
|
||||||
|
const VIEW_MORE_ENVIRONMENT_TAB = "__VIEW_MORE_ENVIRONMENT_TAB__";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TABS_TO_SHOW = 5;
|
||||||
|
|
||||||
|
export const EnvironmentTabs = ({ secretPath }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const currentEnv = useParams({
|
||||||
|
from: ROUTE_PATHS.SecretManager.SecretDashboardPage.id,
|
||||||
|
select: (el) => el.envSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
const isMoreEnvironmentsAllowed =
|
||||||
|
subscription?.environmentLimit && currentWorkspace?.environments
|
||||||
|
? currentWorkspace.environments.length < subscription.environmentLimit
|
||||||
|
: true;
|
||||||
|
|
||||||
|
const [isNavigating, setIsNavigating] = useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
|
"compareEnvironments",
|
||||||
|
"createEnvironment",
|
||||||
|
"upgradePlan"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const selectedIndex = currentWorkspace.environments.findIndex((env) => env.slug === currentEnv);
|
||||||
|
|
||||||
|
let tabEnvironments: WorkspaceEnv[];
|
||||||
|
let dropdownEnvironments: WorkspaceEnv[];
|
||||||
|
|
||||||
|
if (selectedIndex < TABS_TO_SHOW) {
|
||||||
|
tabEnvironments = currentWorkspace.environments.slice(0, TABS_TO_SHOW);
|
||||||
|
dropdownEnvironments = currentWorkspace.environments.slice(TABS_TO_SHOW);
|
||||||
|
} else {
|
||||||
|
tabEnvironments = [
|
||||||
|
...currentWorkspace.environments.slice(0, TABS_TO_SHOW - 1),
|
||||||
|
currentWorkspace.environments[selectedIndex]
|
||||||
|
];
|
||||||
|
dropdownEnvironments = currentWorkspace.environments
|
||||||
|
.slice(TABS_TO_SHOW - 1)
|
||||||
|
.filter((env) => env.slug !== currentEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const handleSelect = async (envSlug: string) => {
|
||||||
|
if (isNavigating) return;
|
||||||
|
|
||||||
|
setIsNavigating(true);
|
||||||
|
await navigate({
|
||||||
|
to: ROUTE_PATHS.SecretManager.SecretDashboardPage.path,
|
||||||
|
params: {
|
||||||
|
envSlug,
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
},
|
||||||
|
search: (prev) => prev
|
||||||
|
});
|
||||||
|
setIsNavigating(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddEnvironment = () => {
|
||||||
|
if (isMoreEnvironmentsAllowed) {
|
||||||
|
handlePopUpOpen("createEnvironment");
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tabs
|
||||||
|
value={currentEnv}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value === COMPARE_ENVIRONMENT_TAB) {
|
||||||
|
handlePopUpOpen("compareEnvironments");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === ADD_ENVIRONMENT_TAB) {
|
||||||
|
handleAddEnvironment();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelect(value);
|
||||||
|
}}
|
||||||
|
defaultValue="environment-tabs"
|
||||||
|
>
|
||||||
|
<TabList>
|
||||||
|
{tabEnvironments.map((environment) => (
|
||||||
|
<Tab className="max-w-[12vw] truncate" value={environment.slug}>
|
||||||
|
<p className="truncate">{environment.name}</p>
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
{dropdownEnvironments.length ? (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Tab value={VIEW_MORE_ENVIRONMENT_TAB} className="p-0">
|
||||||
|
<Tooltip content="More Environments">
|
||||||
|
<div className="px-3">
|
||||||
|
<FontAwesomeIcon icon={faEllipsisH} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Tab>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
|
||||||
|
sideOffset={2}
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>Environments</DropdownMenuLabel>
|
||||||
|
<div className="thin-scrollbar max-h-[40vh] overflow-auto">
|
||||||
|
{dropdownEnvironments.map((environment) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={environment.id}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSelect(environment.slug);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{environment.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="h-1 border-t border-mineshaft-600" />
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={ProjectPermissionSub.Environments}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={handleAddEnvironment}
|
||||||
|
className="data-[highlighted]:bg-mineshaft-900"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Add Environment
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
) : (
|
||||||
|
<Tab value={ADD_ENVIRONMENT_TAB} className="p-0">
|
||||||
|
<Tooltip content="Add environment">
|
||||||
|
<div className="px-3">
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Tab>
|
||||||
|
)}
|
||||||
|
{currentWorkspace.environments.length > 1 && (
|
||||||
|
<Tab className="ml-auto" value={COMPARE_ENVIRONMENT_TAB}>
|
||||||
|
<div className="flex items-center gap-x-2 whitespace-nowrap">
|
||||||
|
<FontAwesomeIcon icon={faArrowRightArrowLeft} />
|
||||||
|
Compare Environments
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
)}
|
||||||
|
</TabList>
|
||||||
|
</Tabs>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.compareEnvironments.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("compareEnvironments", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Compare Environments"
|
||||||
|
subTitle="Compare secrets across multiple environments"
|
||||||
|
className="flex h-full !w-[95vw] max-w-none flex-col"
|
||||||
|
bodyClassName="flex-1 flex flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<CompareEnvironments secretPath={secretPath} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="You can add custom environments if you switch to Infisical's Team plan."
|
||||||
|
/>
|
||||||
|
<AddEnvironmentModal
|
||||||
|
isOpen={popUp.createEnvironment.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("createEnvironment", isOpen)}
|
||||||
|
onComplete={async (env) => {
|
||||||
|
await queryClient.refetchQueries({
|
||||||
|
queryKey: workspaceKeys.getWorkspaceById(currentWorkspace.id)
|
||||||
|
});
|
||||||
|
handleSelect(env.slug);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./EnvironmentTabs";
|
||||||
+68
-66
@@ -3,16 +3,16 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { useCreateWsEnvironment } from "@app/hooks/api";
|
import { useCreateWsEnvironment } from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["createEnv"]>;
|
isOpen: boolean;
|
||||||
handlePopUpClose: (popUpName: keyof UsePopUpState<["createEnv"]>) => void;
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["createEnv"]>, state?: boolean) => void;
|
onComplete?: (environment: WorkspaceEnv) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
@@ -24,10 +24,14 @@ const schema = z.object({
|
|||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
|
type ContentProps = {
|
||||||
|
onComplete: (environment: WorkspaceEnv) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Content = ({ onComplete }: ContentProps) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { mutateAsync, isPending } = useCreateWsEnvironment();
|
const { mutateAsync, isPending } = useCreateWsEnvironment();
|
||||||
const { control, handleSubmit, reset } = useForm<FormData>({
|
const { control, handleSubmit } = useForm<FormData>({
|
||||||
resolver: zodResolver(schema)
|
resolver: zodResolver(schema)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -35,7 +39,7 @@ export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle
|
|||||||
try {
|
try {
|
||||||
if (!currentWorkspace?.id) return;
|
if (!currentWorkspace?.id) return;
|
||||||
|
|
||||||
await mutateAsync({
|
const env = await mutateAsync({
|
||||||
workspaceId: currentWorkspace.id,
|
workspaceId: currentWorkspace.id,
|
||||||
name: environmentName,
|
name: environmentName,
|
||||||
slug: environmentSlug
|
slug: environmentSlug
|
||||||
@@ -46,7 +50,7 @@ export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle
|
|||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePopUpClose("createEnv");
|
onComplete(env);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -57,64 +61,62 @@ export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
isOpen={popUp?.createEnv?.isOpen}
|
<Controller
|
||||||
onOpenChange={(isOpen) => {
|
control={control}
|
||||||
handlePopUpToggle("createEnv", isOpen);
|
defaultValue=""
|
||||||
reset();
|
name="environmentName"
|
||||||
}}
|
render={({ field, fieldState: { error } }) => (
|
||||||
>
|
<FormControl label="Environment Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
<ModalContent title="Create a new environment">
|
<Input {...field} />
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
</FormControl>
|
||||||
<Controller
|
)}
|
||||||
control={control}
|
/>
|
||||||
defaultValue=""
|
<Controller
|
||||||
name="environmentName"
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
defaultValue=""
|
||||||
<FormControl
|
name="environmentSlug"
|
||||||
label="Environment Name"
|
render={({ field, fieldState: { error } }) => (
|
||||||
isError={Boolean(error)}
|
<FormControl
|
||||||
errorText={error?.message}
|
label="Environment Slug"
|
||||||
>
|
helperText="Slugs are shorthands used in cli to access environment"
|
||||||
<Input {...field} />
|
isError={Boolean(error)}
|
||||||
</FormControl>
|
errorText={error?.message}
|
||||||
)}
|
>
|
||||||
/>
|
<Input {...field} />
|
||||||
<Controller
|
</FormControl>
|
||||||
control={control}
|
)}
|
||||||
defaultValue=""
|
/>
|
||||||
name="environmentSlug"
|
<div className="mt-8 flex items-center">
|
||||||
render={({ field, fieldState: { error } }) => (
|
<Button
|
||||||
<FormControl
|
className="mr-4"
|
||||||
label="Environment Slug"
|
size="sm"
|
||||||
helperText="Slugs are shorthands used in cli to access environment"
|
type="submit"
|
||||||
isError={Boolean(error)}
|
isLoading={isPending}
|
||||||
errorText={error?.message}
|
isDisabled={isPending}
|
||||||
>
|
>
|
||||||
<Input {...field} />
|
Create
|
||||||
</FormControl>
|
</Button>
|
||||||
)}
|
<ModalClose asChild>
|
||||||
/>
|
<Button colorSchema="secondary" variant="plain">
|
||||||
<div className="mt-8 flex items-center">
|
Cancel
|
||||||
<Button
|
</Button>
|
||||||
className="mr-4"
|
</ModalClose>
|
||||||
size="sm"
|
</div>
|
||||||
type="submit"
|
</form>
|
||||||
isLoading={isPending}
|
);
|
||||||
isDisabled={isPending}
|
};
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
export const AddEnvironmentModal = ({ onComplete, ...props }: Props) => {
|
||||||
onClick={() => handlePopUpClose("createEnv")}
|
return (
|
||||||
colorSchema="secondary"
|
<Modal {...props}>
|
||||||
variant="plain"
|
<ModalContent title="Create a new environment">
|
||||||
>
|
<Content
|
||||||
Cancel
|
onComplete={(env) => {
|
||||||
</Button>
|
if (onComplete) onComplete(env);
|
||||||
</div>
|
props.onOpenChange(false);
|
||||||
</form>
|
}}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-3
@@ -103,9 +103,8 @@ export const EnvironmentSection = () => {
|
|||||||
<PermissionDeniedBanner />
|
<PermissionDeniedBanner />
|
||||||
)}
|
)}
|
||||||
<AddEnvironmentModal
|
<AddEnvironmentModal
|
||||||
popUp={popUp}
|
isOpen={popUp.createEnv.isOpen}
|
||||||
handlePopUpClose={handlePopUpClose}
|
onOpenChange={(isOpen) => handlePopUpToggle("createEnv", isOpen)}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
/>
|
||||||
<UpdateEnvironmentModal
|
<UpdateEnvironmentModal
|
||||||
popUp={popUp}
|
popUp={popUp}
|
||||||
|
|||||||
Reference in New Issue
Block a user