mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #354 from akhilmhdh/feat/table-loader
Table loading state and empty states
This commit is contained in:
@@ -10,6 +10,6 @@ export const parameters = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
darkMode: {
|
darkMode: {
|
||||||
dark: { ...themes.dark, appContentBg: '#0e1014', appBg: '#0e1014' }
|
dark: { ...themes.dark, appContentBg: 'rgb(14,16,20)', appBg: 'rgb(14,16,20)' }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
20
frontend/src/components/v2/EmptyState/EmptyState.stories.tsx
Normal file
20
frontend/src/components/v2/EmptyState/EmptyState.stories.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { EmptyState } from './EmptyState';
|
||||||
|
|
||||||
|
const meta: Meta<typeof EmptyState> = {
|
||||||
|
title: 'Components/EmptyState',
|
||||||
|
component: EmptyState,
|
||||||
|
tags: ['v2'],
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
title: 'No members found'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof EmptyState>;
|
||||||
|
|
||||||
|
export const Basic: Story = {
|
||||||
|
render: (args) => <EmptyState {...args} />
|
||||||
|
};
|
||||||
21
frontend/src/components/v2/EmptyState/EmptyState.tsx
Normal file
21
frontend/src/components/v2/EmptyState/EmptyState.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { faCubesStacked, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
icon?: IconDefinition;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmptyState = ({ title, className, children, icon = faCubesStacked }: Props) => (
|
||||||
|
<div className={twMerge('flex w-full flex-col items-center px-2 pt-6 text-bunker-300', className)}>
|
||||||
|
<FontAwesomeIcon icon={icon} size="2x" className='mr-4' />
|
||||||
|
<div className='flex flex-row items-center py-4'>
|
||||||
|
<div className="text-bunker-300 text-sm">{title}</div>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
1
frontend/src/components/v2/EmptyState/index.tsx
Normal file
1
frontend/src/components/v2/EmptyState/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { EmptyState } from './EmptyState';
|
||||||
17
frontend/src/components/v2/Skeleton/Skeleton.stories.tsx
Normal file
17
frontend/src/components/v2/Skeleton/Skeleton.stories.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Skeleton } from './Skeleton';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Skeleton> = {
|
||||||
|
title: 'Components/Skeleton',
|
||||||
|
component: Skeleton,
|
||||||
|
tags: ['v2'],
|
||||||
|
argTypes: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Skeleton>;
|
||||||
|
|
||||||
|
export const Basic: Story = {
|
||||||
|
render: (args) => <Skeleton {...args} />
|
||||||
|
};
|
||||||
12
frontend/src/components/v2/Skeleton/Skeleton.tsx
Normal file
12
frontend/src/components/v2/Skeleton/Skeleton.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// To show something is coming up
|
||||||
|
// Can be used with cards
|
||||||
|
// Tables etc
|
||||||
|
export const Skeleton = ({ className }: Props) => (
|
||||||
|
<div className={twMerge('h-6 w-full animate-pulse rounded-md bg-mineshaft-800', className)} />
|
||||||
|
);
|
||||||
1
frontend/src/components/v2/Skeleton/index.tsx
Normal file
1
frontend/src/components/v2/Skeleton/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Skeleton } from './Skeleton';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table';
|
import { Table, TableContainer, TableSkeleton, TBody, Td, Th, THead, Tr } from './Table';
|
||||||
|
|
||||||
const meta: Meta<typeof Table> = {
|
const meta: Meta<typeof Table> = {
|
||||||
title: 'Components/Table',
|
title: 'Components/Table',
|
||||||
@@ -39,3 +39,22 @@ export const Basic: Story = {
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Loading: Story = {
|
||||||
|
render: (args) => (
|
||||||
|
<TableContainer>
|
||||||
|
<Table {...args}>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Head#1</Th>
|
||||||
|
<Th>Head#2</Th>
|
||||||
|
<Th>Head#3</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
<TableSkeleton columns={3} key="story-book-table" />
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { HTMLAttributes, ReactNode, TdHTMLAttributes } from 'react';
|
import { HTMLAttributes, ReactNode, TdHTMLAttributes } from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import { Skeleton } from '../Skeleton';
|
||||||
|
|
||||||
export type TableContainerProps = {
|
export type TableContainerProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
isRounded?: boolean;
|
isRounded?: boolean;
|
||||||
@@ -32,7 +34,7 @@ export type TableProps = {
|
|||||||
export const Table = ({ children, className }: TableProps): JSX.Element => (
|
export const Table = ({ children, className }: TableProps): JSX.Element => (
|
||||||
<table
|
<table
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'w-full rounded rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300',
|
'w-full rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -59,7 +61,10 @@ export type TrProps = {
|
|||||||
} & HTMLAttributes<HTMLTableRowElement>;
|
} & HTMLAttributes<HTMLTableRowElement>;
|
||||||
|
|
||||||
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
|
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
|
||||||
<tr className={twMerge('border border-solid border-mineshaft-700 hover:bg-bunker-700', className)} {...props}>
|
<tr
|
||||||
|
className={twMerge('border border-solid border-mineshaft-700 hover:bg-bunker-700', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -71,7 +76,7 @@ export type ThProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Th = ({ children, className }: ThProps): JSX.Element => (
|
export const Th = ({ children, className }: ThProps): JSX.Element => (
|
||||||
<th className={twMerge('px-5 pt-4 pb-3.5 font-medium font-semibold bg-bunker-500', className)}>{children}</th>
|
<th className={twMerge('bg-bunker-500 px-5 pt-4 pb-3.5 font-semibold', className)}>{children}</th>
|
||||||
);
|
);
|
||||||
|
|
||||||
// table body
|
// table body
|
||||||
@@ -95,3 +100,25 @@ export const Td = ({ children, className, ...props }: TdProps): JSX.Element => (
|
|||||||
{children}
|
{children}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type TBodyLoader = {
|
||||||
|
rows?: number;
|
||||||
|
columns: number;
|
||||||
|
className?: string;
|
||||||
|
// unique key for mapping
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableSkeleton = ({ rows = 3, columns, key, className }: TBodyLoader): JSX.Element => (
|
||||||
|
<>
|
||||||
|
{Array.apply(0, Array(rows)).map((_x, i) => (
|
||||||
|
<Tr key={`${key}-skeleton-rows-${i + 1}`}>
|
||||||
|
{Array.apply(0, Array(columns)).map((_y, j) => (
|
||||||
|
<Td key={`${key}-skeleton-rows-${i + 1}-column-${j + 1}`}>
|
||||||
|
<Skeleton className={className} />
|
||||||
|
</Td>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ export type {
|
|||||||
ThProps,
|
ThProps,
|
||||||
TrProps
|
TrProps
|
||||||
} from './Table';
|
} from './Table';
|
||||||
export { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table';
|
export { Table, TableContainer, TableSkeleton,TBody, Td, Th, THead, Tr } from './Table';
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ export * from './Card';
|
|||||||
export * from './Checkbox';
|
export * from './Checkbox';
|
||||||
export * from './DeleteActionModal';
|
export * from './DeleteActionModal';
|
||||||
export * from './Dropdown';
|
export * from './Dropdown';
|
||||||
|
export * from './EmptyState';
|
||||||
export * from './FormControl';
|
export * from './FormControl';
|
||||||
export * from './IconButton';
|
export * from './IconButton';
|
||||||
export * from './Input';
|
export * from './Input';
|
||||||
export * from './Menu';
|
export * from './Menu';
|
||||||
export * from './Modal';
|
export * from './Modal';
|
||||||
export * from './Select';
|
export * from './Select';
|
||||||
|
export * from './Skeleton';
|
||||||
export * from './Spinner';
|
export * from './Spinner';
|
||||||
export * from './Switch';
|
export * from './Switch';
|
||||||
export * from './Table';
|
export * from './Table';
|
||||||
|
|||||||
@@ -36,10 +36,12 @@ export const OrgSettingsPage = () => {
|
|||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
const orgId = currentOrg?._id || '';
|
const orgId = currentOrg?._id || '';
|
||||||
const { data: orgUsers } = useGetOrgUsers(orgId);
|
const { data: orgUsers, isLoading: isOrgUserLoading } = useGetOrgUsers(orgId);
|
||||||
const { data: workspaceMemberships } = useGetUserWorkspaceMemberships(orgId);
|
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
|
||||||
|
useGetUserWorkspaceMemberships(orgId);
|
||||||
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
|
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
|
||||||
const { data: incidentContact } = useGetOrgIncidentContact(orgId);
|
const { data: incidentContact, isLoading: IsIncidentContactLoading } =
|
||||||
|
useGetOrgIncidentContact(orgId);
|
||||||
|
|
||||||
const renameOrg = useRenameOrg();
|
const renameOrg = useRenameOrg();
|
||||||
const removeUserOrgMembership = useDeleteOrgMembership();
|
const removeUserOrgMembership = useDeleteOrgMembership();
|
||||||
@@ -197,9 +199,9 @@ export const OrgSettingsPage = () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This function deleted a workspace.
|
* This function deleted a workspace.
|
||||||
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
|
* It first checks if there is more than one workspace available. Otherwise, it doesn't delete
|
||||||
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
||||||
* It then deletes the workspace and forwards the user to another aviable workspace.
|
* It then deletes the workspace and forwards the user to another available workspace.
|
||||||
*/
|
*/
|
||||||
// const executeDeletingWorkspace = async () => {
|
// const executeDeletingWorkspace = async () => {
|
||||||
// const userWorkspaces = await getWorkspaces();
|
// const userWorkspaces = await getWorkspaces();
|
||||||
@@ -230,13 +232,11 @@ export const OrgSettingsPage = () => {
|
|||||||
<div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
|
<div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
|
||||||
<OrgNameChangeSection orgName={currentOrg?.name} onOrgNameChange={onRenameOrg} />
|
<OrgNameChangeSection orgName={currentOrg?.name} onOrgNameChange={onRenameOrg} />
|
||||||
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
||||||
<p className="mr-4 text-xl font-semibold text-white">
|
<p className="mr-4 mb-4 text-xl font-semibold text-white">
|
||||||
{t('section-members:org-members')}
|
{t('section-members:org-members')}
|
||||||
</p>
|
</p>
|
||||||
<p className="mr-4 mt-2 mb-2 text-gray-400">
|
|
||||||
{t('section-members:org-members-description')}
|
|
||||||
</p>
|
|
||||||
<OrgMembersTable
|
<OrgMembersTable
|
||||||
|
isLoading={isOrgUserLoading || IsWsMembershipLoading}
|
||||||
isMoreUserNotAllowed={isMoreUsersNotAllowed}
|
isMoreUserNotAllowed={isMoreUsersNotAllowed}
|
||||||
orgName={currentOrg?.name || ''}
|
orgName={currentOrg?.name || ''}
|
||||||
members={orgUsers}
|
members={orgUsers}
|
||||||
@@ -261,6 +261,7 @@ export const OrgSettingsPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<OrgIncidentContactsTable
|
<OrgIncidentContactsTable
|
||||||
|
isLoading={IsIncidentContactLoading}
|
||||||
contacts={incidentContact}
|
contacts={incidentContact}
|
||||||
onRemoveContact={onRemoveIncidentContact}
|
onRemoveContact={onRemoveIncidentContact}
|
||||||
onAddContact={onAddIncidentContact}
|
onAddContact={onAddIncidentContact}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
import {
|
||||||
|
faContactBook,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faPlus,
|
||||||
|
faTrash
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
@@ -8,6 +13,7 @@ import * as yup from 'yup';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
@@ -15,16 +21,17 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr
|
Tr} from '@app/components/v2';
|
||||||
} from '@app/components/v2';
|
|
||||||
import { usePopUp } from '@app/hooks';
|
import { usePopUp } from '@app/hooks';
|
||||||
import { IncidentContact } from '@app/hooks/api/types';
|
import { IncidentContact } from '@app/hooks/api/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
isLoading?: boolean;
|
||||||
contacts?: IncidentContact[];
|
contacts?: IncidentContact[];
|
||||||
onRemoveContact: (email: string) => Promise<void>;
|
onRemoveContact: (email: string) => Promise<void>;
|
||||||
onAddContact: (email: string) => Promise<void>;
|
onAddContact: (email: string) => Promise<void>;
|
||||||
@@ -39,7 +46,8 @@ type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
|
|||||||
export const OrgIncidentContactsTable = ({
|
export const OrgIncidentContactsTable = ({
|
||||||
contacts = [],
|
contacts = [],
|
||||||
onAddContact,
|
onAddContact,
|
||||||
onRemoveContact
|
onRemoveContact,
|
||||||
|
isLoading
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [searchContact, setSearchContact] = useState('');
|
const [searchContact, setSearchContact] = useState('');
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
@@ -66,6 +74,10 @@ export const OrgIncidentContactsTable = ({
|
|||||||
handlePopUpClose('removeContact');
|
handlePopUpClose('removeContact');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredContacts = contacts.filter(({ email }) =>
|
||||||
|
email.toLocaleLowerCase().includes(searchContact)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="mb-4 flex">
|
<div className="mb-4 flex">
|
||||||
@@ -96,28 +108,25 @@ export const OrgIncidentContactsTable = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{contacts
|
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
|
||||||
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
|
{filteredContacts?.map(({ email }) => (
|
||||||
?.map(({ email }) => (
|
<Tr key={email}>
|
||||||
<Tr key={email}>
|
<Td className="w-full">{email}</Td>
|
||||||
<Td className="w-full">{email}</Td>
|
<Td className="mr-4">
|
||||||
<Td className="mr-4">
|
<IconButton
|
||||||
<IconButton
|
ariaLabel="delete"
|
||||||
ariaLabel="delete"
|
colorSchema="danger"
|
||||||
colorSchema="danger"
|
onClick={() => handlePopUpOpen('removeContact', { email })}
|
||||||
onClick={() => handlePopUpOpen('removeContact', { email })}
|
>
|
||||||
>
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
</IconButton>
|
||||||
</IconButton>
|
</Td>
|
||||||
</Td>
|
</Tr>
|
||||||
</Tr>
|
))}
|
||||||
))}
|
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{contacts
|
{filteredContacts?.length === 0 && !isLoading && (
|
||||||
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
|
<EmptyState title="No incident contacts found" icon={faContactBook} />
|
||||||
?.length === 0 && (
|
|
||||||
<div className='py-4 bg-bunker-800 text-sm text-center text-bunker-400 w-full mx-auto flex justify-center'>No incident contacts found</div>
|
|
||||||
)}
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
import { faMagnifyingGlass, faPlus, faTrash, faUsers } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
@@ -8,6 +8,7 @@ import * as yup from 'yup';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
@@ -17,14 +18,14 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
Tag,
|
Tag,
|
||||||
TBody,
|
TBody,
|
||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr,
|
||||||
UpgradePlanModal
|
UpgradePlanModal} from '@app/components/v2';
|
||||||
} from '@app/components/v2';
|
|
||||||
import { usePopUp } from '@app/hooks';
|
import { usePopUp } from '@app/hooks';
|
||||||
import { OrgUser, Workspace } from '@app/hooks/api/types';
|
import { OrgUser, Workspace } from '@app/hooks/api/types';
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ type Props = {
|
|||||||
members?: OrgUser[];
|
members?: OrgUser[];
|
||||||
workspaceMemberships?: Record<string, Workspace[]>;
|
workspaceMemberships?: Record<string, Workspace[]>;
|
||||||
orgName: string;
|
orgName: string;
|
||||||
|
isLoading?: boolean;
|
||||||
isMoreUserNotAllowed: boolean;
|
isMoreUserNotAllowed: boolean;
|
||||||
onRemoveMember: (userId: string) => Promise<void>;
|
onRemoveMember: (userId: string) => Promise<void>;
|
||||||
onInviteMember: (email: string) => Promise<void>;
|
onInviteMember: (email: string) => Promise<void>;
|
||||||
@@ -56,7 +58,8 @@ export const OrgMembersTable = ({
|
|||||||
onInviteMember,
|
onInviteMember,
|
||||||
onGrantAccess,
|
onGrantAccess,
|
||||||
onRoleChange,
|
onRoleChange,
|
||||||
userId
|
userId,
|
||||||
|
isLoading
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [searchMemberFilter, setSearchMemberFilter] = useState('');
|
const [searchMemberFilter, setSearchMemberFilter] = useState('');
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
@@ -72,8 +75,8 @@ export const OrgMembersTable = ({
|
|||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
||||||
|
|
||||||
const onAddMember = ({ email }: TAddMemberForm) => {
|
const onAddMember = async ({ email }: TAddMemberForm) => {
|
||||||
onInviteMember(email);
|
await onInviteMember(email);
|
||||||
handlePopUpClose('addMember');
|
handlePopUpClose('addMember');
|
||||||
reset();
|
reset();
|
||||||
};
|
};
|
||||||
@@ -140,73 +143,79 @@ export const OrgMembersTable = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
{isLoading && <TableSkeleton columns={5} key="org-members" />}
|
||||||
const name = user ? `${user.firstName} ${user.lastName}` : '-';
|
{!isLoading &&
|
||||||
const email = user?.email || inviteEmail;
|
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
||||||
const userWs = workspaceMemberships?.[user?._id];
|
const name = user ? `${user.firstName} ${user.lastName}` : '-';
|
||||||
|
const email = user?.email || inviteEmail;
|
||||||
|
const userWs = workspaceMemberships?.[user?._id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
|
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
<Td>{email}</Td>
|
<Td>{email}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{status === 'accepted' && (
|
{status === 'accepted' && (
|
||||||
<Select
|
<Select
|
||||||
defaultValue={role}
|
defaultValue={role}
|
||||||
isDisabled={userId === user?._id}
|
isDisabled={userId === user?._id}
|
||||||
className="w-full bg-mineshaft-600"
|
className="w-full bg-mineshaft-600"
|
||||||
onValueChange={(selectedRole) =>
|
onValueChange={(selectedRole) =>
|
||||||
onRoleChange(orgMembershipId, selectedRole)
|
onRoleChange(orgMembershipId, selectedRole)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(isIamOwner || role === 'owner') && (
|
{(isIamOwner || role === 'owner') && (
|
||||||
<SelectItem value="owner">owner</SelectItem>
|
<SelectItem value="owner">owner</SelectItem>
|
||||||
)}
|
)}
|
||||||
<SelectItem value="admin">admin</SelectItem>
|
<SelectItem value="admin">admin</SelectItem>
|
||||||
<SelectItem value="member">member</SelectItem>
|
<SelectItem value="member">member</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{(status === 'invited' || status === 'verified') && (
|
{(status === 'invited' || status === 'verified') && (
|
||||||
<Button colorSchema="secondary" onClick={() => onInviteMember(email)}>
|
<Button colorSchema="secondary" onClick={() => onInviteMember(email)}>
|
||||||
Resent Invite
|
Resent Invite
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{status === 'completed' && (
|
{status === 'completed' && (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
onClick={() => onGrantAccess(user?._id, user?.publicKey)}
|
onClick={() => onGrantAccess(user?._id, user?.publicKey)}
|
||||||
>
|
>
|
||||||
Grant Access
|
Grant Access
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{userWs ? (
|
{userWs ? (
|
||||||
userWs?.map(({ name: wsName, _id }) => (
|
userWs?.map(({ name: wsName, _id }) => (
|
||||||
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
|
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
|
||||||
{wsName}
|
{wsName}
|
||||||
</Tag>
|
</Tag>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Tag colorSchema="red">This user isn't part of any projects yet</Tag>
|
<Tag colorSchema="red">This user isn't part of any projects yet</Tag>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{userId !== user?._id && <IconButton
|
{userId !== user?._id && (
|
||||||
ariaLabel="delete"
|
<IconButton
|
||||||
colorSchema="danger"
|
ariaLabel="delete"
|
||||||
isDisabled={userId === user?._id}
|
colorSchema="danger"
|
||||||
onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })}
|
isDisabled={userId === user?._id}
|
||||||
>
|
onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })}
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
>
|
||||||
</IconButton>}
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
</Td>
|
</IconButton>
|
||||||
</Tr>
|
)}
|
||||||
);
|
</Td>
|
||||||
})}
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{filterdUser.length === 0 && <tr className='bg-bunker-800 text-sm py-4 text-center text-bunker-400 w-full mx-auto flex justify-center'><td className='col-span-5'>No project members found</td></tr>}
|
{!isLoading && filterdUser?.length === 0 && (
|
||||||
|
<EmptyState title="No project members found" icon={faUsers} />
|
||||||
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -45,11 +45,9 @@ import {
|
|||||||
|
|
||||||
export const ProjectSettingsPage = () => {
|
export const ProjectSettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentWorkspace, workspaces } = useWorkspace();
|
const { currentWorkspace, workspaces, isLoading: isWorkspaceLoading } = useWorkspace();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: serviceTokens } = useGetUserWsServiceTokens({
|
|
||||||
workspaceID: currentWorkspace?._id || ''
|
|
||||||
});
|
|
||||||
const workspaceID = currentWorkspace?._id || '';
|
const workspaceID = currentWorkspace?._id || '';
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
// delete action worksapce
|
// delete action worksapce
|
||||||
@@ -66,12 +64,15 @@ export const ProjectSettingsPage = () => {
|
|||||||
const deleteWsEnv = useDeleteWsEnvironment();
|
const deleteWsEnv = useDeleteWsEnvironment();
|
||||||
|
|
||||||
// service token
|
// service token
|
||||||
|
const { data: serviceTokens, isLoading: isServiceTokenLoading } = useGetUserWsServiceTokens({
|
||||||
|
workspaceID: currentWorkspace?._id || ''
|
||||||
|
});
|
||||||
const { data: latestFileKey } = useGetUserWsKey(workspaceID);
|
const { data: latestFileKey } = useGetUserWsKey(workspaceID);
|
||||||
const createServiceToken = useCreateServiceToken();
|
const createServiceToken = useCreateServiceToken();
|
||||||
const deleteServiceToken = useDeleteServiceToken();
|
const deleteServiceToken = useDeleteServiceToken();
|
||||||
|
|
||||||
// tag
|
// tag
|
||||||
const { data: wsTags } = useGetWsTags(workspaceID);
|
const { data: wsTags, isLoading: isTagLoading } = useGetWsTags(workspaceID);
|
||||||
const createWsTag = useCreateWsTag();
|
const createWsTag = useCreateWsTag();
|
||||||
const deleteWsTag = useDeleteWsTag();
|
const deleteWsTag = useDeleteWsTag();
|
||||||
|
|
||||||
@@ -300,7 +301,7 @@ export const ProjectSettingsPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex flex-col px-8 text-mineshaft-50 dark dark:[color-scheme:dark]">
|
<div className="dark container mx-auto flex flex-col px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
{/* TODO(akhilmhdh): Remove this right when layout is refactored */}
|
{/* TODO(akhilmhdh): Remove this right when layout is refactored */}
|
||||||
<div className="relative right-5">
|
<div className="relative right-5">
|
||||||
<NavHeader pageName={t('settings-project:title')} isProjectRelated />
|
<NavHeader pageName={t('settings-project:title')} isProjectRelated />
|
||||||
@@ -319,6 +320,7 @@ export const ProjectSettingsPage = () => {
|
|||||||
/>
|
/>
|
||||||
<CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} />
|
<CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} />
|
||||||
<EnvironmentSection
|
<EnvironmentSection
|
||||||
|
isLoading={isWorkspaceLoading}
|
||||||
environments={currentWorkspace?.environments || []}
|
environments={currentWorkspace?.environments || []}
|
||||||
onCreate={onCreateWsEnv}
|
onCreate={onCreateWsEnv}
|
||||||
onDelete={onDeleteWsEnv}
|
onDelete={onDeleteWsEnv}
|
||||||
@@ -326,6 +328,7 @@ export const ProjectSettingsPage = () => {
|
|||||||
isEnvServiceAllowed={isEnvServiceAllowed}
|
isEnvServiceAllowed={isEnvServiceAllowed}
|
||||||
/>
|
/>
|
||||||
<ServiceTokenSection
|
<ServiceTokenSection
|
||||||
|
isLoading={isServiceTokenLoading}
|
||||||
tokens={serviceTokens || []}
|
tokens={serviceTokens || []}
|
||||||
environments={currentWorkspace?.environments || []}
|
environments={currentWorkspace?.environments || []}
|
||||||
onDeleteToken={onDeleteServiceToken}
|
onDeleteToken={onDeleteServiceToken}
|
||||||
@@ -333,6 +336,7 @@ export const ProjectSettingsPage = () => {
|
|||||||
onCreateToken={onCreateServiceToken}
|
onCreateToken={onCreateServiceToken}
|
||||||
/>
|
/>
|
||||||
<SecretTagsSection
|
<SecretTagsSection
|
||||||
|
isLoading={isTagLoading}
|
||||||
tags={wsTags || []}
|
tags={wsTags || []}
|
||||||
onDeleteTag={onDeleteTag}
|
onDeleteTag={onDeleteTag}
|
||||||
workspaceName={currentWorkspace?.name || ''}
|
workspaceName={currentWorkspace?.name || ''}
|
||||||
|
|||||||
@@ -13,22 +13,18 @@ export const AutoCapitalizationSection = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<form>
|
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
|
||||||
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
|
<p className="mb-4 mt-2 text-xl font-semibold">{t('settings-project:auto-capitalization')}</p>
|
||||||
<p className="mb-4 mt-2 text-xl font-semibold">
|
<Checkbox
|
||||||
{t('settings-project:auto-capitalization')}
|
className="data-[state=checked]:bg-primary"
|
||||||
</p>
|
id="autoCapitalization"
|
||||||
<Checkbox
|
isChecked={workspaceAutoCapitalization}
|
||||||
className="data-[state=checked]:bg-primary"
|
onCheckedChange={(state) => {
|
||||||
id="autoCapitalization"
|
onAutoCapitalizationChange(state as boolean);
|
||||||
isChecked={workspaceAutoCapitalization}
|
}}
|
||||||
onCheckedChange={(state) => {
|
>
|
||||||
onAutoCapitalizationChange(state as boolean);
|
{t('settings-project:auto-capitalization-description')}
|
||||||
}}
|
</Checkbox>
|
||||||
>
|
</div>
|
||||||
{t('settings-project:auto-capitalization-description')}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as yup from 'yup';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
@@ -25,6 +27,7 @@ import { usePopUp } from '@app/hooks/usePopUp';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
environments: Array<{ name: string; slug: string }>;
|
environments: Array<{ name: string; slug: string }>;
|
||||||
|
isLoading?: boolean;
|
||||||
isEnvServiceAllowed: boolean;
|
isEnvServiceAllowed: boolean;
|
||||||
onCreate: (data: CreateUpdateEnvFormData) => Promise<void>;
|
onCreate: (data: CreateUpdateEnvFormData) => Promise<void>;
|
||||||
onUpdate: (oldEnvSlug: string, data: CreateUpdateEnvFormData) => Promise<void>;
|
onUpdate: (oldEnvSlug: string, data: CreateUpdateEnvFormData) => Promise<void>;
|
||||||
@@ -43,6 +46,7 @@ export const EnvironmentSection = ({
|
|||||||
isEnvServiceAllowed,
|
isEnvServiceAllowed,
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
isLoading,
|
||||||
onUpdate
|
onUpdate
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
@@ -116,7 +120,8 @@ export const EnvironmentSection = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{environments?.length > 0 ? (
|
{isLoading && <TableSkeleton columns={3} key="project-envs" />}
|
||||||
|
{!isLoading &&
|
||||||
environments.map(({ name, slug }) => (
|
environments.map(({ name, slug }) => (
|
||||||
<Tr key={name}>
|
<Tr key={name}>
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
@@ -152,11 +157,11 @@ export const EnvironmentSection = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
))
|
))}
|
||||||
) : (
|
{!isLoading && environments?.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={4} className="pt-7 pb-5 text-center text-bunker-400">
|
<Td colSpan={3}>
|
||||||
No environments found
|
<EmptyState title="No environments found" />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
|
import { faPlus, faTags, faTrashCan } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
@@ -7,6 +7,7 @@ import * as yup from 'yup';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
@@ -16,23 +17,24 @@ import {
|
|||||||
ModalTrigger,
|
ModalTrigger,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr} from '@app/components/v2';
|
||||||
} from '@app/components/v2';
|
|
||||||
import { usePopUp } from '@app/hooks';
|
import { usePopUp } from '@app/hooks';
|
||||||
import { WorkspaceTag } from '@app/hooks/api/types';
|
import { WorkspaceTag } from '@app/hooks/api/types';
|
||||||
|
|
||||||
const createTagSchema = yup.object({
|
const createTagSchema = yup.object({
|
||||||
name: yup.string().required().label('Tag Name'),
|
name: yup.string().required().label('Tag Name')
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateWsTag = yup.InferType<typeof createTagSchema>;
|
export type CreateWsTag = yup.InferType<typeof createTagSchema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tags: WorkspaceTag[];
|
tags: WorkspaceTag[];
|
||||||
|
isLoading?: boolean;
|
||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
onDeleteTag: (tagID: string) => Promise<void>;
|
onDeleteTag: (tagID: string) => Promise<void>;
|
||||||
onCreateTag: (data: CreateWsTag) => Promise<string>;
|
onCreateTag: (data: CreateWsTag) => Promise<string>;
|
||||||
@@ -42,6 +44,7 @@ type DeleteModalData = { name: string; id: string };
|
|||||||
|
|
||||||
export const SecretTagsSection = ({
|
export const SecretTagsSection = ({
|
||||||
tags = [],
|
tags = [],
|
||||||
|
isLoading,
|
||||||
onDeleteTag,
|
onDeleteTag,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
onCreateTag
|
onCreateTag
|
||||||
@@ -76,7 +79,10 @@ export const SecretTagsSection = ({
|
|||||||
<div className="flex w-full flex-row justify-between">
|
<div className="flex w-full flex-row justify-between">
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
|
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
|
||||||
<p className="text-sm text-gray-400">Every secret can be assigned to one or more tags. Here you can add and remove tags for the current project.</p>
|
<p className="text-sm text-gray-400">
|
||||||
|
Every secret can be assigned to one or more tags. Here you can add and remove tags for
|
||||||
|
the current project.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -92,8 +98,8 @@ export const SecretTagsSection = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</ModalTrigger>
|
</ModalTrigger>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Add a tag for ${ workspaceName}`}
|
title={`Add a tag for ${workspaceName}`}
|
||||||
subTitle='Specify your tag name, and the slug will be created automatically.'
|
subTitle="Specify your tag name, and the slug will be created automatically."
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -102,7 +108,7 @@ export const SecretTagsSection = ({
|
|||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label='Tag Name'
|
label="Tag Name"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
@@ -130,7 +136,7 @@ export const SecretTagsSection = ({
|
|||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TableContainer className='mt-4'>
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
@@ -140,7 +146,8 @@ export const SecretTagsSection = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{tags?.length > 0 ? (
|
{isLoading && <TableSkeleton columns={3} key="secret-tags" />}
|
||||||
|
{!isLoading &&
|
||||||
tags.map(({ _id, name, slug }) => (
|
tags.map(({ _id, name, slug }) => (
|
||||||
<Tr key={name}>
|
<Tr key={name}>
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
@@ -149,7 +156,7 @@ export const SecretTagsSection = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handlePopUpOpen('deleteTagConfirmation', {
|
handlePopUpOpen('deleteTagConfirmation', {
|
||||||
name,
|
name,
|
||||||
id: _id
|
id: _id
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -160,11 +167,11 @@ export const SecretTagsSection = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
))
|
))}
|
||||||
) : (
|
{!isLoading && tags?.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={4} className="py-6 text-center text-bunker-400">
|
<Td colSpan={3}>
|
||||||
No tags found for this project
|
<EmptyState title="No secret tags found" icon={faTags} />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { faCheck, faCopy, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck, faCopy, faKey, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
@@ -9,6 +9,7 @@ import * as yup from 'yup';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
@@ -47,6 +49,7 @@ export type CreateServiceToken = yup.InferType<typeof createServiceTokenSchema>;
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tokens: ServiceToken[];
|
tokens: ServiceToken[];
|
||||||
|
isLoading?: boolean;
|
||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
environments: WorkspaceEnv[];
|
environments: WorkspaceEnv[];
|
||||||
onDeleteToken: (serviceTokenID: string) => Promise<void>;
|
onDeleteToken: (serviceTokenID: string) => Promise<void>;
|
||||||
@@ -57,6 +60,7 @@ type DeleteModalData = { name: string; id: string };
|
|||||||
|
|
||||||
export const ServiceTokenSection = ({
|
export const ServiceTokenSection = ({
|
||||||
tokens = [],
|
tokens = [],
|
||||||
|
isLoading,
|
||||||
onDeleteToken,
|
onDeleteToken,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
environments = [],
|
environments = [],
|
||||||
@@ -252,7 +256,7 @@ export const ServiceTokenSection = ({
|
|||||||
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
|
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
|
||||||
title={`Delete ${
|
title={`Delete ${
|
||||||
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || ' '
|
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || ' '
|
||||||
} api key?`}
|
} service token?`}
|
||||||
onChange={(isOpen) => handlePopUpToggle('deleteAPITokenConfirmation', isOpen)}
|
onChange={(isOpen) => handlePopUpToggle('deleteAPITokenConfirmation', isOpen)}
|
||||||
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
|
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
|
||||||
onClose={() => handlePopUpClose('deleteAPITokenConfirmation')}
|
onClose={() => handlePopUpClose('deleteAPITokenConfirmation')}
|
||||||
@@ -269,7 +273,8 @@ export const ServiceTokenSection = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{tokens?.length > 0 ? (
|
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
|
||||||
|
{!isLoading &&
|
||||||
tokens.map((row) => (
|
tokens.map((row) => (
|
||||||
<Tr key={row._id}>
|
<Tr key={row._id}>
|
||||||
<Td>{row.name}</Td>
|
<Td>{row.name}</Td>
|
||||||
@@ -290,11 +295,11 @@ export const ServiceTokenSection = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
))
|
))}
|
||||||
) : (
|
{!isLoading && tokens?.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={4} className="py-6 text-center text-bunker-400">
|
<Td colSpan={4} className="py-6 text-center text-bunker-400">
|
||||||
No service tokens found
|
<EmptyState title="No service tokens found" icon={faKey} />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1340,163 +1340,165 @@ module.exports = {
|
|||||||
900: '#176437',
|
900: '#176437',
|
||||||
DEFAULT: '#2ecc71'
|
DEFAULT: '#2ecc71'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
type: {
|
||||||
|
'0%': { transform: 'translateX(0ch)' },
|
||||||
|
'5%, 10%': { transform: 'translateX(1ch)' },
|
||||||
|
'15%, 20%': { transform: 'translateX(2ch)' },
|
||||||
|
'25%, 30%': { transform: 'translateX(3ch)' },
|
||||||
|
'35%, 40%': { transform: 'translateX(4ch)' },
|
||||||
|
'45%, 50%': { transform: 'translateX(5ch)' },
|
||||||
|
'55%, 60%': { transform: 'translateX(6ch)' },
|
||||||
|
'65%, 70%': { transform: 'translateX(7ch)' },
|
||||||
|
'75%, 80%': { transform: 'translateX(8ch)' },
|
||||||
|
'85%, 90%': { transform: 'translateX(9ch)' },
|
||||||
|
'95%, 100%': { transform: 'translateX(11ch)' }
|
||||||
|
},
|
||||||
|
// REQUIRED BY DESIGN COMPONENT
|
||||||
|
// MODAL
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: 0 },
|
||||||
|
'100%': { opacity: 1 }
|
||||||
|
},
|
||||||
|
popIn: {
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translate(-50%, -48%) scale(0.96)'
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translate(-50%, -50%) scale(1)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Dropdown
|
||||||
|
slideUpAndFade: {
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: ' translateY(2px)'
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: ' translateY(0)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
slideRightAndFade: {
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: ' translateX(-2px)'
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: ' translateX(0)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
slideDownAndFade: {
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: ' translateY(-2px)'
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: ' translateY(0)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
slideLeftAndFade: {
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: ' translateX(2px)'
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: ' translateX(0)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// END
|
||||||
|
spin: {
|
||||||
|
'0%': { transform: 'rotate(0deg)' },
|
||||||
|
'40%': { transform: 'rotate(360deg)' },
|
||||||
|
'100%': { transform: 'rotate(360deg)' }
|
||||||
|
},
|
||||||
|
bounce: {
|
||||||
|
'0%': { transform: 'translateY(-90%)' },
|
||||||
|
'100%': { transform: 'translateY(-100%)' }
|
||||||
|
},
|
||||||
|
wiggle: {
|
||||||
|
'0%, 100%': { transform: 'rotate(-3deg)' },
|
||||||
|
'50%': { transform: 'rotate(3deg)' }
|
||||||
|
},
|
||||||
|
ping: {
|
||||||
|
'75%, 100%': {
|
||||||
|
transform: 'scale(2)',
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popup: {
|
||||||
|
'0%': {
|
||||||
|
transform: 'scale(0.2)',
|
||||||
|
opacity: 0
|
||||||
|
// transform: "translateY(120%)",
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'scale(1)',
|
||||||
|
opacity: 1
|
||||||
|
// transform: "translateY(100%)",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popright: {
|
||||||
|
'0%': {
|
||||||
|
transform: 'translateX(-100%)'
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'translateX(0%)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popleft: {
|
||||||
|
'0%': {
|
||||||
|
transform: 'translateX(100%)'
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'translateX(0%)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popdown: {
|
||||||
|
'0%': {
|
||||||
|
transform: 'scale(0.2)',
|
||||||
|
opacity: 0
|
||||||
|
// transform: "translateY(80%)",
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'scale(1)',
|
||||||
|
opacity: 1
|
||||||
|
// transform: "translateY(100%)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
// Design Lib
|
||||||
|
// MODAL
|
||||||
|
fadeIn: 'fadeIn 100ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
popIn: 'popIn 150ms cubic-bezier(0.16, 1, 0.3, 1);',
|
||||||
|
// Dropdown
|
||||||
|
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
// END
|
||||||
|
// TODO:(akhilmhdh) remove all these unused and keep the config file as small as possible
|
||||||
|
// Make the whole color pallelte into simpler
|
||||||
|
bounce: 'bounce 1000ms ease-in-out infinite',
|
||||||
|
spin: 'spin 4000ms ease-in-out infinite',
|
||||||
|
cursor: 'cursor .6s linear infinite alternate',
|
||||||
|
type: 'type 2.7s ease-out .8s infinite alternate both',
|
||||||
|
'type-reverse': 'type 1.8s ease-out 0s infinite alternate-reverse both',
|
||||||
|
wiggle: 'wiggle 200ms ease-in-out',
|
||||||
|
ping: 'ping 1000ms ease-in-out infinite',
|
||||||
|
popup: 'popup 300ms ease-in-out',
|
||||||
|
popdown: 'popdown 300ms ease-in-out',
|
||||||
|
popright: 'popright 100ms ease-in-out',
|
||||||
|
popleft: 'popleft 100ms ease-in-out'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyframes: {
|
|
||||||
type: {
|
|
||||||
'0%': { transform: 'translateX(0ch)' },
|
|
||||||
'5%, 10%': { transform: 'translateX(1ch)' },
|
|
||||||
'15%, 20%': { transform: 'translateX(2ch)' },
|
|
||||||
'25%, 30%': { transform: 'translateX(3ch)' },
|
|
||||||
'35%, 40%': { transform: 'translateX(4ch)' },
|
|
||||||
'45%, 50%': { transform: 'translateX(5ch)' },
|
|
||||||
'55%, 60%': { transform: 'translateX(6ch)' },
|
|
||||||
'65%, 70%': { transform: 'translateX(7ch)' },
|
|
||||||
'75%, 80%': { transform: 'translateX(8ch)' },
|
|
||||||
'85%, 90%': { transform: 'translateX(9ch)' },
|
|
||||||
'95%, 100%': { transform: 'translateX(11ch)' }
|
|
||||||
},
|
|
||||||
// REQUIRED BY DEISGN COMPONENT
|
|
||||||
// MODAL
|
|
||||||
fadeIn: {
|
|
||||||
'0%': { opacity: 0 },
|
|
||||||
'100%': { opacity: 1 }
|
|
||||||
},
|
|
||||||
popIn: {
|
|
||||||
from: {
|
|
||||||
opacity: 0,
|
|
||||||
transform: 'translate(-50%, -48%) scale(0.96)'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
opacity: 1,
|
|
||||||
transform: 'translate(-50%, -50%) scale(1)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Dropdown
|
|
||||||
slideUpAndFade: {
|
|
||||||
from: {
|
|
||||||
opacity: 0,
|
|
||||||
transform: ' translateY(2px)'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
opacity: 1,
|
|
||||||
transform: ' translateY(0)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
slideRightAndFade: {
|
|
||||||
from: {
|
|
||||||
opacity: 0,
|
|
||||||
transform: ' translateX(-2px)'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
opacity: 1,
|
|
||||||
transform: ' translateX(0)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
slideDownAndFade: {
|
|
||||||
from: {
|
|
||||||
opacity: 0,
|
|
||||||
transform: ' translateY(-2px)'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
opacity: 1,
|
|
||||||
transform: ' translateY(0)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
slideLeftAndFade: {
|
|
||||||
from: {
|
|
||||||
opacity: 0,
|
|
||||||
transform: ' translateX(2px)'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
opacity: 1,
|
|
||||||
transform: ' translateX(0)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// END
|
|
||||||
spin: {
|
|
||||||
'0%': { transform: 'rotate(0deg)' },
|
|
||||||
'40%': { transform: 'rotate(360deg)' },
|
|
||||||
'100%': { transform: 'rotate(360deg)' }
|
|
||||||
},
|
|
||||||
bounce: {
|
|
||||||
'0%': { transform: 'translateY(-90%)' },
|
|
||||||
'100%': { transform: 'translateY(-100%)' }
|
|
||||||
},
|
|
||||||
wiggle: {
|
|
||||||
'0%, 100%': { transform: 'rotate(-3deg)' },
|
|
||||||
'50%': { transform: 'rotate(3deg)' }
|
|
||||||
},
|
|
||||||
ping: {
|
|
||||||
'75%, 100%': {
|
|
||||||
transform: 'scale(2)',
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
popup: {
|
|
||||||
'0%': {
|
|
||||||
transform: 'scale(0.2)',
|
|
||||||
opacity: 0
|
|
||||||
// transform: "translateY(120%)",
|
|
||||||
},
|
|
||||||
'100%': {
|
|
||||||
transform: 'scale(1)',
|
|
||||||
opacity: 1
|
|
||||||
// transform: "translateY(100%)",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
popright: {
|
|
||||||
'0%': {
|
|
||||||
transform: 'translateX(-100%)'
|
|
||||||
},
|
|
||||||
'100%': {
|
|
||||||
transform: 'translateX(0%)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
popleft: {
|
|
||||||
'0%': {
|
|
||||||
transform: 'translateX(100%)'
|
|
||||||
},
|
|
||||||
'100%': {
|
|
||||||
transform: 'translateX(0%)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
popdown: {
|
|
||||||
'0%': {
|
|
||||||
transform: 'scale(0.2)',
|
|
||||||
opacity: 0
|
|
||||||
// transform: "translateY(80%)",
|
|
||||||
},
|
|
||||||
'100%': {
|
|
||||||
transform: 'scale(1)',
|
|
||||||
opacity: 1
|
|
||||||
// transform: "translateY(100%)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
// Design Lib
|
|
||||||
// MODAL
|
|
||||||
fadeIn: 'fadeIn 100ms cubic-bezier(0.16, 1, 0.3, 1)',
|
|
||||||
popIn: 'popIn 150ms cubic-bezier(0.16, 1, 0.3, 1);',
|
|
||||||
// Dropdown
|
|
||||||
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
|
||||||
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
|
||||||
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
|
||||||
slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
|
||||||
// END
|
|
||||||
bounce: 'bounce 1000ms ease-in-out infinite',
|
|
||||||
spin: 'spin 4000ms ease-in-out infinite',
|
|
||||||
cursor: 'cursor .6s linear infinite alternate',
|
|
||||||
type: 'type 2.7s ease-out .8s infinite alternate both',
|
|
||||||
'type-reverse': 'type 1.8s ease-out 0s infinite alternate-reverse both',
|
|
||||||
wiggle: 'wiggle 200ms ease-in-out',
|
|
||||||
ping: 'ping 1000ms ease-in-out infinite',
|
|
||||||
popup: 'popup 300ms ease-in-out',
|
|
||||||
popdown: 'popdown 300ms ease-in-out',
|
|
||||||
popright: 'popright 100ms ease-in-out',
|
|
||||||
popleft: 'popleft 100ms ease-in-out'
|
|
||||||
},
|
|
||||||
fontSize: {
|
fontSize: {
|
||||||
xxxs: '.23rem',
|
xxxs: '.23rem',
|
||||||
xxs: '.5rem',
|
xxs: '.5rem',
|
||||||
|
|||||||
Reference in New Issue
Block a user