Merge remote-tracking branch 'origin/main' into feat/moved-mfa-to-org-level

This commit is contained in:
Sheen Capadngan
2024-10-23 00:04:09 +08:00
128 changed files with 1889 additions and 953 deletions

View File

@@ -115,7 +115,10 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
formState: { isSubmitting },
handleSubmit
} = useForm<FormData>({
resolver: zodResolver(createTagSchema)
resolver: zodResolver(createTagSchema),
defaultValues: {
color: secretTagsColors[0].hex
}
});
const { currentWorkspace } = useWorkspace();

View File

@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { faCheck, faMinus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { twMerge } from "tailwind-merge";
@@ -15,6 +15,7 @@ export type CheckboxProps = Omit<
isRequired?: boolean;
checkIndicatorBg?: string | undefined;
isError?: boolean;
isIndeterminate?: boolean;
};
export const Checkbox = ({
@@ -26,6 +27,7 @@ export const Checkbox = ({
isRequired,
checkIndicatorBg,
isError,
isIndeterminate,
...props
}: CheckboxProps): JSX.Element => {
return (
@@ -45,7 +47,11 @@ export const Checkbox = ({
id={id}
>
<CheckboxPrimitive.Indicator className={`${checkIndicatorBg || "text-bunker-800"}`}>
<FontAwesomeIcon icon={faCheck} size="sm" />
{isIndeterminate ? (
<FontAwesomeIcon icon={faMinus} size="sm" />
) : (
<FontAwesomeIcon icon={faCheck} size="sm" />
)}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
<label

View File

@@ -1,4 +1,4 @@
import { HTMLAttributes, ReactNode, TdHTMLAttributes } from "react";
import { DetailedHTMLProps, HTMLAttributes, ReactNode, TdHTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import { Skeleton } from "../Skeleton";
@@ -7,12 +7,13 @@ export type TableContainerProps = {
children: ReactNode;
isRounded?: boolean;
className?: string;
};
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
export const TableContainer = ({
children,
className,
isRounded = true
isRounded = true,
...props
}: TableContainerProps): JSX.Element => (
<div
className={twMerge(
@@ -20,6 +21,7 @@ export const TableContainer = ({
isRounded && "rounded-lg",
className
)}
{...props}
>
{children}
</div>

View File

@@ -10,13 +10,13 @@ export const groupKeys = {
slug,
offset,
limit,
username
search
}: {
slug: string;
offset: number;
limit: number;
username: string;
}) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, username }] as const
search: string;
}) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, search }] as const
};
type TUser = {
@@ -33,27 +33,28 @@ export const useListGroupUsers = ({
groupSlug,
offset = 0,
limit = 10,
username
search
}: {
id: string;
groupSlug: string;
offset: number;
limit: number;
username: string;
search: string;
}) => {
return useQuery({
queryKey: groupKeys.specificGroupUserMemberships({
slug: groupSlug,
offset,
limit,
username
search
}),
enabled: Boolean(groupSlug),
keepPreviousData: true,
queryFn: async () => {
const params = new URLSearchParams({
offset: String(offset),
limit: String(limit),
username
search
});
const { data } = await apiRequest.get<{ users: TUser[]; totalCount: number }>(

View File

@@ -48,7 +48,7 @@ export const fetchProjectSecrets = async ({
};
export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
const personalSecrets: Record<string, { id: string; value?: string }> = {};
const personalSecrets: Record<string, { id: string; value?: string; env: string }> = {};
const secrets: SecretV3RawSanitized[] = [];
rawSecrets.forEach((el) => {
const decryptedSecret: SecretV3RawSanitized = {
@@ -69,7 +69,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
if (el.type === SecretType.Personal) {
personalSecrets[decryptedSecret.key] = {
id: el.id,
value: el.secretValue
value: el.secretValue,
env: el.environment
};
} else {
secrets.push(decryptedSecret);
@@ -77,9 +78,10 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
});
secrets.forEach((sec) => {
if (personalSecrets?.[sec.key]) {
sec.idOverride = personalSecrets[sec.key].id;
sec.valueOverride = personalSecrets[sec.key].value;
const personalSecret = personalSecrets?.[sec.key];
if (personalSecret && personalSecret.env === sec.env) {
sec.idOverride = personalSecret.id;
sec.valueOverride = personalSecret.value;
sec.overrideAction = "modified";
}
});

View File

@@ -3,6 +3,7 @@ export { useLeaveConfirm } from "./useLeaveConfirm";
export { usePagination } from "./usePagination";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";
export { useResetPageHelper } from "./useResetPageHelper";
export { useSyntaxHighlight } from "./useSyntaxHighlight";
export { useTimedReset } from "./useTimedReset";
export { useToggle } from "./useToggle";

View File

@@ -0,0 +1,16 @@
import { Dispatch, SetStateAction, useEffect } from "react";
export const useResetPageHelper = ({
totalCount,
offset,
setPage
}: {
totalCount: number;
offset: number;
setPage: Dispatch<SetStateAction<number>>;
}) => {
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
};

View File

@@ -241,6 +241,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
(!orgs?.map((org) => org.id)?.includes(router.query.id as string) &&
!router.asPath.includes("project") &&
!router.asPath.includes("personal") &&
!router.asPath.includes("secret-scanning") &&
!router.asPath.includes("integration")))
) {
router.push(`/org/${currentOrg?.id}/overview`);

View File

@@ -72,7 +72,8 @@ const SecretScanning = withPermission(
</div>
{config.isSecretScanningDisabled && (
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
We are working on improving the performance of secret scanning due to increased usage.
We are working on improving the performance of secret scanning due to increased
usage.
</NoticeBanner>
)}
<div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
@@ -116,7 +117,7 @@ const SecretScanning = withPermission(
colorSchema="primary"
onClick={generateNewIntegrationSession}
className="h-min py-2"
isDisabled={!isAllowed || config.isSecretScanningDisabled}
isDisabled={!isAllowed}
>
Integrate with GitHub
</Button>

View File

@@ -21,6 +21,7 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useDebounce, useResetPageHelper } from "@app/hooks";
import { useAddUserToGroup, useListGroupUsers, useRemoveUserFromGroup } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -33,18 +34,28 @@ export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const [debouncedSearch] = useDebounce(searchMemberFilter);
const popUpData = popUp?.groupMembers?.data as {
groupId: string;
slug: string;
};
const offset = (page - 1) * perPage;
const { data, isLoading } = useListGroupUsers({
id: popUpData?.groupId,
groupSlug: popUpData?.slug,
offset: (page - 1) * perPage,
offset,
limit: perPage,
username: searchMemberFilter
search: debouncedSearch
});
const { totalCount = 0 } = data ?? {};
useResetPageHelper({
totalCount,
offset,
setPage
});
const { mutateAsync: assignMutateAsync } = useAddUserToGroup();
@@ -140,9 +151,9 @@ export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
})}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && (
{!isLoading && totalCount > 0 && (
<Pagination
count={data.totalCount}
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
@@ -150,7 +161,10 @@ export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
/>
)}
{!isLoading && !data?.users?.length && (
<EmptyState title="No users found" icon={faUsers} />
<EmptyState
title={debouncedSearch ? "No users match search" : "No users found"}
icon={faUsers}
/>
)}
</TableContainer>
</ModalContent>

View File

@@ -1,5 +1,11 @@
import { useState } from "react";
import { faEllipsis, faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons";
import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faEllipsis,
faMagnifyingGlass,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
@@ -11,6 +17,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Select,
SelectItem,
@@ -24,7 +31,9 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -43,12 +52,21 @@ type Props = {
) => void;
};
enum GroupsOrderBy {
Name = "name",
Slug = "slug",
Role = "role"
}
export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
const [searchGroupsFilter, setSearchGroupsFilter] = useState("");
const [debouncedSearch] = useDebounce(searchGroupsFilter.trim());
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { isLoading, data: groups } = useGetOrganizationGroups(orgId);
const { mutateAsync: updateMutateAsync } = useUpdateGroup();
const [orderBy, setOrderBy] = useState(GroupsOrderBy.Name);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const { data: roles } = useGetOrgRoles(orgId);
@@ -72,6 +90,43 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
}
};
const filteredGroups = useMemo(() => {
const filtered = debouncedSearch
? groups?.filter(
({ name, slug }) =>
name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
slug.toLowerCase().includes(debouncedSearch.toLowerCase())
)
: groups;
const ordered = filtered?.sort((a, b) => {
switch (orderBy) {
case GroupsOrderBy.Role: {
const aValue = a.role === "custom" ? (a.customRole?.name as string) : a.role;
const bValue = b.role === "custom" ? (b.customRole?.name as string) : b.role;
return aValue.toLowerCase().localeCompare(bValue.toLowerCase());
}
default:
return a[orderBy].toLowerCase().localeCompare(b[orderBy].toLowerCase());
}
});
return orderDirection === OrderByDirection.ASC ? ordered : ordered?.reverse();
}, [debouncedSearch, groups, orderBy, orderDirection]);
const handleSort = (column: GroupsOrderBy) => {
if (column === orderBy) {
setOrderDirection((prev) =>
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
);
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
return (
<div>
<Input
@@ -84,16 +139,70 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>Role</Th>
<Th>
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={`ml-2 ${orderBy === GroupsOrderBy.Name ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(GroupsOrderBy.Name)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC && orderBy === GroupsOrderBy.Name
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Slug
<IconButton
variant="plain"
className={`ml-2 ${orderBy === GroupsOrderBy.Slug ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(GroupsOrderBy.Slug)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC && orderBy === GroupsOrderBy.Slug
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Role
<IconButton
variant="plain"
className={`ml-2 ${orderBy === GroupsOrderBy.Role ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(GroupsOrderBy.Role)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC && orderBy === GroupsOrderBy.Role
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-groups" />}
{!isLoading &&
groups?.map(({ id, name, slug, role, customRole }) => {
filteredGroups?.map(({ id, name, slug, role, customRole }) => {
return (
<Tr className="h-10" key={`org-group-${id}`}>
<Td>{name}</Td>
@@ -226,7 +335,12 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
})}
</TBody>
</Table>
{groups?.length === 0 && <EmptyState title="No groups found" icon={faUsers} />}
{filteredGroups?.length === 0 && (
<EmptyState
title={groups?.length === 0 ? "No groups found" : "No groups match search"}
icon={faUsers}
/>
)}
</TableContainer>
</div>
);

View File

@@ -270,7 +270,7 @@ export const IdentityKubernetesAuthForm = ({
label="Allowed Namespaces"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any namespaces."
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
>
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
</FormControl>

View File

@@ -1,4 +1,3 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import {
faArrowDown,
@@ -34,7 +33,7 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePagination } from "@app/hooks";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types";
@@ -87,10 +86,11 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
);
const { totalCount = 0 } = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
useResetPageHelper({
totalCount,
offset,
setPage
});
const { data: roles } = useGetOrgRoles(organizationId);

View File

@@ -377,7 +377,14 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
</TBody>
</Table>
{!isLoading && filterdUser?.length === 0 && (
<EmptyState title="No organization members found" icon={faUsers} />
<EmptyState
title={
members?.length === 0
? "No organization members found"
: "No organization members match search"
}
icon={faUsers}
/>
)}
</TableContainer>
</div>

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */
import { z } from "zod";
import { OrgPermissionSubjects } from "@app/context";
import { TPermission } from "@app/hooks/api/roles/types";
const generalPermissionSchema = z
@@ -46,7 +47,8 @@ export const formSchema = z.object({
ldap: generalPermissionSchema,
billing: generalPermissionSchema,
identity: generalPermissionSchema,
"organization-admin-console": adminConsolePermissionSchmea
"organization-admin-console": adminConsolePermissionSchmea,
[OrgPermissionSubjects.Kms]: generalPermissionSchema
})
.optional()
});

View File

@@ -3,7 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api";
import {
formRolePermission2API,
@@ -64,6 +64,10 @@ const SIMPLE_PERMISSION_OPTIONS = [
{
title: "SCIM",
formName: "scim"
},
{
title: "External KMS",
formName: OrgPermissionSubjects.Kms
}
] as const;

View File

@@ -1,4 +1,3 @@
import { useEffect } from "react";
import Link from "next/link";
import {
faArrowDown,
@@ -51,7 +50,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { usePagination, usePopUp } from "@app/hooks";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks";
import { CmekOrderBy, TCmek } from "@app/hooks/api/cmeks/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
@@ -108,10 +107,11 @@ export const CmekTable = () => {
});
const { keys = [], totalCount = 0 } = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
useResetPageHelper({
totalCount,
offset,
setPage
});
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upsertKey",

View File

@@ -1,4 +1,3 @@
import { useEffect } from "react";
import Link from "next/link";
import {
faArrowDown,
@@ -44,7 +43,7 @@ import {
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePagination } from "@app/hooks";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { IdentityMembership } from "@app/hooks/api/identities/types";
@@ -99,10 +98,11 @@ export const IdentityTab = withProjectPermission(
const { totalCount = 0 } = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
useResetPageHelper({
totalCount,
offset,
setPage
});
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();

View File

@@ -2,29 +2,33 @@ import { createContext, ReactNode, useContext, useEffect, useRef } from "react";
import { useRouter } from "next/router";
import { createStore, StateCreator, StoreApi, useStore } from "zustand";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
// akhilmhdh: Don't remove this file if ur thinking why use zustand just for selected selects state
// This is first step and the whole secret crud will be moved to this global page scope state
// this will allow more stuff like undo grouping stuffs etc
type SelectedSecretState = {
selectedSecret: Record<string, boolean>;
selectedSecret: Record<string, SecretV3RawSanitized>;
action: {
toggle: (id: string) => void;
toggle: (secret: SecretV3RawSanitized) => void;
reset: () => void;
set: (secrets: Record<string, SecretV3RawSanitized>) => void;
};
};
const createSelectedSecretStore: StateCreator<SelectedSecretState> = (set) => ({
selectedSecret: {},
action: {
toggle: (id) =>
toggle: (secret) =>
set((state) => {
const isChecked = Boolean(state.selectedSecret?.[id]);
const isChecked = Boolean(state.selectedSecret?.[secret.id]);
const newChecks = { ...state.selectedSecret };
// remove selection if its present else add it
if (isChecked) delete newChecks[id];
else newChecks[id] = true;
if (isChecked) delete newChecks[secret.id];
else newChecks[secret.id] = secret;
return { selectedSecret: newChecks };
}),
reset: () => set({ selectedSecret: {} })
reset: () => set({ selectedSecret: {} }),
set: (secrets) => set({ selectedSecret: secrets })
}
});

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { subject } from "@casl/ability";
@@ -9,14 +9,14 @@ import { twMerge } from "tailwind-merge";
import NavHeader from "@app/components/navigation/NavHeader";
import { createNotification } from "@app/components/notifications";
import { PermissionDeniedBanner } from "@app/components/permissions";
import { ContentLoader, Pagination } from "@app/components/v2";
import { Checkbox, ContentLoader, Pagination, Tooltip } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useDebounce, usePagination, usePopUp } from "@app/hooks";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import {
useGetImportedSecretsSingleEnv,
useGetSecretApprovalPolicyOfABoard,
@@ -39,7 +39,11 @@ import { PitDrawer } from "./components/PitDrawer";
import { SecretDropzone } from "./components/SecretDropzone";
import { SecretListView } from "./components/SecretListView";
import { SnapshotView } from "./components/SnapshotView";
import { StoreProvider } from "./SecretMainPage.store";
import {
StoreProvider,
useSelectedSecretActions,
useSelectedSecrets
} from "./SecretMainPage.store";
import { Filter, RowType } from "./SecretMainPage.types";
const LOADER_TEXT = [
@@ -48,7 +52,7 @@ const LOADER_TEXT = [
"Getting secret import links..."
];
export const SecretMainPage = () => {
const SecretMainPageContent = () => {
const { t } = useTranslation();
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
const router = useRouter();
@@ -160,10 +164,11 @@ export const SecretMainPage = () => {
totalCount = 0
} = data ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
useResetPageHelper({
totalCount,
offset,
setPage
});
// fetch imported secrets to show user the overriden ones
const { data: importedSecrets } = useGetImportedSecretsSingleEnv({
@@ -283,6 +288,33 @@ export const SecretMainPage = () => {
}
}, [secretPath]);
const selectedSecrets = useSelectedSecrets();
const selectedSecretActions = useSelectedSecretActions();
const allRowsSelectedOnPage = useMemo(() => {
if (secrets?.every((secret) => selectedSecrets[secret.id]))
return { isChecked: true, isIndeterminate: false };
if (secrets?.some((secret) => selectedSecrets[secret.id]))
return { isChecked: true, isIndeterminate: true };
return { isChecked: false, isIndeterminate: false };
}, [selectedSecrets, secrets]);
const toggleSelectAllRows = () => {
const newChecks = { ...selectedSecrets };
secrets?.forEach((secret) => {
if (allRowsSelectedOnPage.isChecked) {
delete newChecks[secret.id];
} else {
newChecks[secret.id] = secret;
}
});
selectedSecretActions.set(newChecks);
};
if (isDetailsLoading) {
return <ContentLoader text={LOADER_TEXT} />;
}
@@ -300,169 +332,192 @@ export const SecretMainPage = () => {
};
return (
<StoreProvider>
<div className="container mx-auto flex flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<SecretV2MigrationSection />
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
currentEnv={environment}
userAvailableEnvs={currentWorkspace?.environments}
isFolderMode
<div className="container mx-auto flex flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<SecretV2MigrationSection />
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
currentEnv={environment}
userAvailableEnvs={currentWorkspace?.environments}
isFolderMode
secretPath={secretPath}
isProjectRelated
onEnvChange={handleEnvChange}
isProtectedBranch={isProtectedBranch}
protectionPolicyName={boardPolicy?.name}
/>
</div>
{!isRollbackMode ? (
<>
<ActionBar
environment={environment}
workspaceId={workspaceId}
projectSlug={projectSlug}
secretPath={secretPath}
isProjectRelated
onEnvChange={handleEnvChange}
isProtectedBranch={isProtectedBranch}
protectionPolicyName={boardPolicy?.name}
isVisible={isVisible}
filter={filter}
tags={tags}
onVisibilityToggle={handleToggleVisibility}
onSearchChange={handleSearchChange}
onToggleTagFilter={handleTagToggle}
snapshotCount={snapshotCount || 0}
isSnapshotCountLoading={isSnapshotCountLoading}
onToggleRowType={handleToggleRowType}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
/>
</div>
{!isRollbackMode ? (
<>
<ActionBar
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
projectSlug={projectSlug}
secretPath={secretPath}
isVisible={isVisible}
filter={filter}
tags={tags}
onVisibilityToggle={handleToggleVisibility}
onSearchChange={handleSearchChange}
onToggleTagFilter={handleTagToggle}
snapshotCount={snapshotCount || 0}
isSnapshotCountLoading={isSnapshotCountLoading}
onToggleRowType={handleToggleRowType}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
/>
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
<div className="flex flex-col" id="dashboard">
{isNotEmpty && (
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
<div className="flex flex-col" id="dashboard">
{isNotEmpty && (
<div
className={twMerge(
"sticky top-0 flex border-b border-mineshaft-600 bg-mineshaft-800 font-medium"
)}
>
<div
className={twMerge(
"sticky top-0 flex border-b border-mineshaft-600 bg-mineshaft-800 font-medium"
)}
className="flex w-80 flex-shrink-0 items-center border-r border-mineshaft-600 px-4 py-2"
role="button"
tabIndex={0}
onClick={handleSortToggle}
onKeyDown={(evt) => {
if (evt.key === "Enter") handleSortToggle();
}}
>
<div style={{ width: "2.8rem" }} className="flex-shrink-0 px-4 py-3" />
<div
className="flex w-80 flex-shrink-0 items-center border-r border-mineshaft-600 px-4 py-2"
role="button"
tabIndex={0}
onClick={handleSortToggle}
onKeyDown={(evt) => {
if (evt.key === "Enter") handleSortToggle();
}}
<Tooltip
className="max-w-[20rem] whitespace-nowrap"
content={
totalCount > 0
? `${
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
} all secrets on page`
: ""
}
>
Key
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
className="ml-2"
/>
</div>
<div className="flex-grow px-4 py-2">Value</div>
<div className="mr-6 ml-1">
<Checkbox
isDisabled={totalCount === 0}
id="checkbox-select-all-rows"
onClick={(e) => e.stopPropagation()}
isChecked={allRowsSelectedOnPage.isChecked}
isIndeterminate={allRowsSelectedOnPage.isIndeterminate}
onCheckedChange={toggleSelectAllRows}
/>
</div>
</Tooltip>
Key
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
className="ml-2"
/>
</div>
)}
{canReadSecret && imports?.length && (
<SecretImportListView
searchTerm={debouncedSearchFilter}
secretImports={imports}
isFetching={isDetailsFetching}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
importedSecrets={importedSecrets}
/>
)}
{folders?.length && (
<FolderListView
folders={folders}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
onNavigateToFolder={handleResetFilter}
/>
)}
{canReadSecret && dynamicSecrets?.length && (
<DynamicSecretListView
environment={environment}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecrets={dynamicSecrets}
/>
)}
{canReadSecret && secrets?.length && (
<SecretListView
secrets={secrets}
tags={tags}
isVisible={isVisible}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
/>
)}
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
</div>
<div className="flex-grow px-4 py-2">Value</div>
</div>
)}
{canReadSecret && imports?.length && (
<SecretImportListView
searchTerm={debouncedSearchFilter}
secretImports={imports}
isFetching={isDetailsFetching}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
importedSecrets={importedSecrets}
/>
)}
{folders?.length && (
<FolderListView
folders={folders}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
onNavigateToFolder={handleResetFilter}
/>
)}
{canReadSecret && dynamicSecrets?.length && (
<DynamicSecretListView
environment={environment}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecrets={dynamicSecrets}
/>
)}
{canReadSecret && secrets?.length && (
<SecretListView
secrets={secrets}
tags={tags}
isVisible={isVisible}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
/>
)}
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
</div>
{!isDetailsLoading && totalCount > 0 && (
<Pagination
startAdornment={
<SecretTableResourceCount
dynamicSecretCount={totalDynamicSecretCount}
importCount={totalImportCount}
secretCount={totalSecretCount}
folderCount={totalFolderCount}
/>
}
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
<CreateSecretForm
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
autoCapitalize={currentWorkspace?.autoCapitalization}
isProtectedBranch={isProtectedBranch}
</div>
{!isDetailsLoading && totalCount > 0 && (
<Pagination
startAdornment={
<SecretTableResourceCount
dynamicSecretCount={totalDynamicSecretCount}
importCount={totalImportCount}
secretCount={totalSecretCount}
folderCount={totalFolderCount}
/>
}
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
<SecretDropzone
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isSmaller={isNotEmpty}
environments={currentWorkspace?.environments}
isProtectedBranch={isProtectedBranch}
/>
<PitDrawer
secretSnaphots={snapshotList}
snapshotId={snapshotId}
isDrawerOpen={popUp.snapshots.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("snapshots", isOpen)}
hasNextPage={hasNextSnapshotListPage}
fetchNextPage={fetchNextSnapshotList}
onSelectSnapshot={handleSelectSnapshot}
isFetchingNextPage={isFetchingNextSnapshotList}
/>
</>
) : (
<SnapshotView
snapshotId={snapshotId || ""}
)}
<CreateSecretForm
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
secrets={secrets}
folders={folders}
snapshotCount={snapshotCount}
onGoBack={handleResetSnapshot}
onClickListSnapshot={() => handlePopUpToggle("snapshots", true)}
autoCapitalize={currentWorkspace?.autoCapitalization}
isProtectedBranch={isProtectedBranch}
/>
)}
</div>
</StoreProvider>
<SecretDropzone
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isSmaller={isNotEmpty}
environments={currentWorkspace?.environments}
isProtectedBranch={isProtectedBranch}
/>
<PitDrawer
secretSnaphots={snapshotList}
snapshotId={snapshotId}
isDrawerOpen={popUp.snapshots.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("snapshots", isOpen)}
hasNextPage={hasNextSnapshotListPage}
fetchNextPage={fetchNextSnapshotList}
onSelectSnapshot={handleSelectSnapshot}
isFetchingNextPage={isFetchingNextSnapshotList}
/>
</>
) : (
<SnapshotView
snapshotId={snapshotId || ""}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
secrets={secrets}
folders={folders}
snapshotCount={snapshotCount}
onGoBack={handleResetSnapshot}
onClickListSnapshot={() => handlePopUpToggle("snapshots", true)}
/>
)}
</div>
);
};
export const SecretMainPage = () => (
<StoreProvider>
<SecretMainPageContent />
</StoreProvider>
);

View File

@@ -54,7 +54,7 @@ import {
import { usePopUp } from "@app/hooks";
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
import { SecretType, SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { SecretType, WsTag } from "@app/hooks/api/types";
import {
PopUpNames,
@@ -69,8 +69,7 @@ import { FolderForm } from "./FolderForm";
import { MoveSecretsModal } from "./MoveSecretsModal";
type Props = {
secrets?: SecretV3RawSanitized[];
// swtich the secrets type as it gets decrypted after api call
// switch the secrets type as it gets decrypted after api call
environment: string;
// @depreciated will be moving all these details to zustand
workspaceId: string;
@@ -89,7 +88,6 @@ type Props = {
};
export const ActionBar = ({
secrets = [],
environment,
workspaceId,
projectSlug,
@@ -202,7 +200,7 @@ export const ActionBar = ({
};
const handleSecretBulkDelete = async () => {
const bulkDeletedSecrets = secrets.filter(({ id }) => Boolean(selectedSecrets?.[id]));
const bulkDeletedSecrets = Object.values(selectedSecrets);
try {
await deleteBatchSecretV3({
secretPath,
@@ -235,7 +233,7 @@ export const ActionBar = ({
shouldOverwrite: boolean;
}) => {
try {
const secretsToMove = secrets.filter(({ id }) => Boolean(selectedSecrets?.[id]));
const secretsToMove = Object.values(selectedSecrets);
const { isDestinationUpdated, isSourceUpdated } = await moveSecrets({
projectSlug,
shouldOverwrite,
@@ -553,7 +551,7 @@ export const ActionBar = ({
<FontAwesomeIcon icon={faMinusSquare} size="lg" />
</IconButton>
</Tooltip>
<div className="ml-4 flex-grow px-2 text-sm">
<div className="ml-2 flex-grow px-2 text-sm">
{Object.keys(selectedSecrets).length} Selected
</div>
<ProjectPermissionCan

View File

@@ -158,17 +158,17 @@ export const SecretImportItem = ({
}
}}
>
<div className="flex w-12 items-center px-4 py-2 text-green-700">
<div className="flex w-11 items-center py-2 pl-5 text-green-700">
<FontAwesomeIcon icon={faFileImport} />
</div>
<div className="flex flex-grow items-center px-4 py-2">
<div className="flex flex-grow items-center py-2 pl-4 pr-2">
<EnvFolderIcon
env={importEnv.slug || ""}
secretPath={secretImport?.importPath || ""}
// isReplication={isReplication}
/>
</div>
<div className="flex items-center space-x-4 px-4 py-2">
<div className="flex items-center space-x-4 py-2 pr-4">
{lastReplicated && (
<Tooltip
position="left"

View File

@@ -56,7 +56,7 @@ type Props = {
onDetailViewSecret: (sec: SecretV3RawSanitized) => void;
isVisible?: boolean;
isSelected?: boolean;
onToggleSecretSelect: (id: string) => void;
onToggleSecretSelect: (secret: SecretV3RawSanitized) => void;
tags: WsTag[];
onCreateTag: () => void;
environment: string;
@@ -218,7 +218,7 @@ export const SecretItem = memo(
<Checkbox
id={`checkbox-${secret.id}`}
isChecked={isSelected}
onCheckedChange={() => onToggleSecretSelect(secret.id)}
onCheckedChange={() => onToggleSecretSelect(secret)}
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
/>
<FontAwesomeSymbol

View File

@@ -304,7 +304,7 @@ export const SecretListView = ({
environment={environment}
secretPath={secretPath}
tags={wsTags}
isSelected={selectedSecrets?.[secret.id]}
isSelected={Boolean(selectedSecrets?.[secret.id])}
onToggleSecretSelect={toggleSelectedSecret}
isVisible={isVisible}
secret={secret}

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -25,6 +25,7 @@ import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
Checkbox,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -54,7 +55,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { useDebounce, usePagination, usePopUp } from "@app/hooks";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import {
useCreateFolder,
useCreateSecretV3,
@@ -67,7 +68,7 @@ import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
import { SecretType, TSecretFolder } from "@app/hooks/api/types";
import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
import { SecretOverviewDynamicSecretRow } from "@app/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow";
@@ -106,18 +107,10 @@ export const SecretOverviewPage = () => {
const { t } = useTranslation();
const router = useRouter();
// this is to set expandable table width
// coz when overflow the table goes to the right
const parentTableRef = useRef<HTMLTableElement>(null);
const [expandableTableWidth, setExpandableTableWidth] = useState(0);
const [scrollOffset, setScrollOffset] = useState(0);
const [debouncedScrollOffset] = useDebounce(scrollOffset);
const { permission } = useProjectPermission();
useEffect(() => {
if (parentTableRef.current) {
setExpandableTableWidth(parentTableRef.current.clientWidth);
}
}, [parentTableRef.current]);
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
const isProjectV3 = currentWorkspace?.version === ProjectVersion.V3;
const { currentOrg } = useOrganization();
@@ -133,8 +126,9 @@ export const SecretOverviewPage = () => {
>(new Map());
const [selectedEntries, setSelectedEntries] = useState<{
[EntryType.FOLDER]: Record<string, boolean>;
[EntryType.SECRET]: Record<string, boolean>;
// selectedEntries[name/key][envSlug][resource]
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
}>({
[EntryType.FOLDER]: {},
[EntryType.SECRET]: {}
@@ -152,23 +146,6 @@ export const SecretOverviewPage = () => {
orderBy
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name);
const toggleSelectedEntry = useCallback(
(type: EntryType, key: string) => {
const isChecked = Boolean(selectedEntries[type]?.[key]);
const newChecks = { ...selectedEntries };
// remove selection if its present else add it
if (isChecked) {
delete newChecks[type][key];
} else {
newChecks[type][key] = true;
}
setSelectedEntries(newChecks);
},
[selectedEntries]
);
const resetSelectedEntries = useCallback(() => {
setSelectedEntries({
[EntryType.FOLDER]: {},
@@ -177,19 +154,13 @@ export const SecretOverviewPage = () => {
}, []);
useEffect(() => {
const handleParentTableWidthResize = () => {
setExpandableTableWidth(parentTableRef.current?.clientWidth || 0);
};
const onRouteChangeStart = () => {
resetSelectedEntries();
};
router.events.on("routeChangeStart", onRouteChangeStart);
window.addEventListener("resize", handleParentTableWidthResize);
return () => {
window.removeEventListener("resize", handleParentTableWidthResize);
router.events.off("routeChangeStart", onRouteChangeStart);
};
}, []);
@@ -242,10 +213,11 @@ export const SecretOverviewPage = () => {
totalCount = 0
} = overview ?? {};
useEffect(() => {
// reset page if no longer valid
if (totalCount <= offset) setPage(1);
}, [totalCount]);
useResetPageHelper({
totalCount,
offset,
setPage
});
const { folderNames, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders);
@@ -551,6 +523,85 @@ export const SecretOverviewPage = () => {
[]
);
const allRowsSelectedOnPage = useMemo(() => {
if (!secrets?.length && !folders?.length) return { isChecked: false, isIndeterminate: false };
if (
(!secrets?.length ||
secrets?.every((secret) => selectedEntries[EntryType.SECRET][secret.key])) &&
(!folders?.length ||
folders?.every((folder) => selectedEntries[EntryType.FOLDER][folder.name]))
)
return { isChecked: true, isIndeterminate: false };
if (
secrets?.some((secret) => selectedEntries[EntryType.SECRET][secret.key]) ||
folders?.some((folder) => selectedEntries[EntryType.FOLDER][folder.name])
)
return { isChecked: true, isIndeterminate: true };
return { isChecked: false, isIndeterminate: false };
}, [selectedEntries, secrets, folders]);
const toggleSelectedEntry = useCallback(
(type: EntryType, key: string) => {
const isChecked = Boolean(selectedEntries[type]?.[key]);
const newChecks = { ...selectedEntries };
// remove selection if its present else add it
if (isChecked) {
delete newChecks[type][key];
} else {
newChecks[type][key] = {};
userAvailableEnvs.forEach((env) => {
const resource =
type === EntryType.SECRET
? getSecretByKey(env.slug, key)
: getFolderByNameAndEnv(key, env.slug);
if (resource) newChecks[type][key][env.slug] = resource;
});
}
setSelectedEntries(newChecks);
},
[selectedEntries, getFolderByNameAndEnv, getSecretByKey]
);
const toggleSelectAllRows = () => {
const newChecks = { ...selectedEntries };
userAvailableEnvs.forEach((env) => {
secrets?.forEach((secret) => {
if (allRowsSelectedOnPage.isChecked) {
delete newChecks[EntryType.SECRET][secret.key];
} else {
if (!newChecks[EntryType.SECRET][secret.key])
newChecks[EntryType.SECRET][secret.key] = {};
const resource = getSecretByKey(env.slug, secret.key);
if (resource) newChecks[EntryType.SECRET][secret.key][env.slug] = resource;
}
});
folders?.forEach((folder) => {
if (allRowsSelectedOnPage.isChecked) {
delete newChecks[EntryType.FOLDER][folder.name];
} else {
if (!newChecks[EntryType.FOLDER][folder.name])
newChecks[EntryType.FOLDER][folder.name] = {};
const resource = getFolderByNameAndEnv(folder.name, env.slug);
if (resource) newChecks[EntryType.FOLDER][folder.name][env.slug] = resource;
}
});
});
setSelectedEntries(newChecks);
};
if (isWorkspaceLoading || (isProjectV3 && isOverviewLoading)) {
return (
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
@@ -799,18 +850,39 @@ export const SecretOverviewPage = () => {
</div>
<SelectionPanel
secretPath={secretPath}
getSecretByKey={getSecretByKey}
getFolderByNameAndEnv={getFolderByNameAndEnv}
selectedEntries={selectedEntries}
resetSelectedEntries={resetSelectedEntries}
/>
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
<TableContainer className="rounded-b-none">
<div className="thin-scrollbar mt-4">
<TableContainer
onScroll={(e) => setScrollOffset(e.currentTarget.scrollLeft)}
className="thin-scrollbar"
>
<Table>
<THead>
<Tr className="sticky top-0 z-20 border-0">
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0">
<div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-3.5 pb-3">
<div className="flex items-center border-b border-r border-mineshaft-600 pr-5 pl-3 pt-3.5 pb-3">
<Tooltip
className="max-w-[20rem] whitespace-nowrap capitalize"
content={
totalCount > 0
? `${
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
} all folders and secrets on page`
: ""
}
>
<div className="mr-4 ml-2">
<Checkbox
isDisabled={totalCount === 0}
id="checkbox-select-all-rows"
isChecked={allRowsSelectedOnPage.isChecked}
isIndeterminate={allRowsSelectedOnPage.isIndeterminate}
onCheckedChange={toggleSelectAllRows}
/>
</div>
</Tooltip>
Name
<IconButton
variant="plain"
@@ -938,7 +1010,7 @@ export const SecretOverviewPage = () => {
<SecretOverviewFolderRow
folderName={folderName}
isFolderPresentInEnv={isFolderPresentInEnv}
isSelected={selectedEntries.folder[folderName]}
isSelected={Boolean(selectedEntries.folder[folderName])}
onToggleFolderSelect={() =>
toggleSelectedEntry(EntryType.FOLDER, folderName)
}
@@ -960,7 +1032,7 @@ export const SecretOverviewPage = () => {
))}
{secKeys.map((key, index) => (
<SecretOverviewTableRow
isSelected={selectedEntries.secret[key]}
isSelected={Boolean(selectedEntries.secret[key])}
onToggleSecretSelect={() => toggleSelectedEntry(EntryType.SECRET, key)}
secretPath={secretPath}
getImportedSecretByKey={getImportedSecretByKey}
@@ -972,7 +1044,7 @@ export const SecretOverviewPage = () => {
environments={visibleEnvs}
secretKey={key}
getSecretByKey={getSecretByKey}
expandableColWidth={expandableTableWidth}
scrollOffset={debouncedScrollOffset}
/>
))}
</>

View File

@@ -23,7 +23,6 @@ type Props = {
secretKey: string;
secretPath: string;
environments: { name: string; slug: string }[];
expandableColWidth: number;
isSelected: boolean;
onToggleSecretSelect: (key: string) => void;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
@@ -41,6 +40,7 @@ type Props = {
env: string,
secretName: string
) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined;
scrollOffset: number;
};
export const SecretOverviewTableRow = ({
@@ -53,9 +53,7 @@ export const SecretOverviewTableRow = ({
onSecretDelete,
isImportedSecretPresentInEnv,
getImportedSecretByKey,
// temporary until below todo is resolved
// eslint-disable-next-line @typescript-eslint/no-unused-vars
expandableColWidth,
scrollOffset,
onToggleSecretSelect,
isSelected
}: Props) => {
@@ -152,11 +150,12 @@ export const SecretOverviewTableRow = ({
}`}
>
<div
className="ml-2 w-[99%] p-2"
// TODO: scott expandableColWidth sometimes 0 due to parent ref not mounting, opting for relative width until resolved
// style={{
// width: `calc(${expandableColWidth} - 1rem)`
// }}
className="ml-2 p-2"
style={{
marginLeft: scrollOffset,
width: "calc(100vw - 300px)", // 300px accounts for sidebar and margin
maxWidth: "calc(1536px - 50px)" // tw container max width minus padding for uw displays
}}
>
<SecretRenameRow
secretKey={secretKey}

View File

@@ -27,31 +27,23 @@ export enum EntryType {
type Props = {
secretPath: string;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
getFolderByNameAndEnv: (name: string, env: string) => TSecretFolder | undefined;
resetSelectedEntries: () => void;
selectedEntries: {
[EntryType.FOLDER]: Record<string, boolean>;
[EntryType.SECRET]: Record<string, boolean>;
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
};
};
export const SelectionPanel = ({
getFolderByNameAndEnv,
getSecretByKey,
secretPath,
resetSelectedEntries,
selectedEntries
}: Props) => {
export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntries }: Props) => {
const { permission } = useProjectPermission();
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"bulkDeleteEntries"
] as const);
const selectedFolderCount = Object.keys(selectedEntries.folder).length
const selectedKeysCount = Object.keys(selectedEntries.secret).length
const selectedCount = selectedFolderCount + selectedKeysCount
const selectedFolderCount = Object.keys(selectedEntries.folder).length;
const selectedKeysCount = Object.keys(selectedEntries.secret).length;
const selectedCount = selectedFolderCount + selectedKeysCount;
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
@@ -77,7 +69,7 @@ export const SelectionPanel = ({
return "Do you want to delete the selected secrets across environments?";
}
return "Do you want to delete the selected folders across environments?";
}
};
const handleBulkDelete = async () => {
let processedEntries = 0;
@@ -94,8 +86,8 @@ export const SelectionPanel = ({
}
await Promise.all(
Object.keys(selectedEntries.folder).map(async (folderName) => {
const folder = getFolderByNameAndEnv(folderName, env.slug);
Object.values(selectedEntries.folder).map(async (folderRecord) => {
const folder = folderRecord[env.slug];
if (folder) {
processedEntries += 1;
await deleteFolder({
@@ -108,9 +100,9 @@ export const SelectionPanel = ({
})
);
const secretsToDelete = Object.keys(selectedEntries.secret).reduce(
(accum: TDeleteSecretBatchDTO["secrets"], secretName) => {
const entry = getSecretByKey(env.slug, secretName);
const secretsToDelete = Object.values(selectedEntries.secret).reduce(
(accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => {
const entry = secretRecord[env.slug];
if (entry) {
return [
...accum,
@@ -173,7 +165,7 @@ export const SelectionPanel = ({
<FontAwesomeIcon icon={faMinusSquare} size="lg" />
</IconButton>
</Tooltip>
<div className="ml-4 flex-grow px-2 text-sm">{selectedCount} Selected</div>
<div className="ml-1 flex-grow px-2 text-sm">{selectedCount} Selected</div>
{shouldShowDelete && (
<Button
variant="outline_bg"