Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

@ -0,0 +1,44 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCopy } from '@/ui/display/icon';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Button } from '@/ui/input/button/components/Button';
import { TextInput } from '@/ui/input/components/TextInput';
const StyledContainer = styled.div`
display: flex;
flex-direction: row;
`;
const StyledLinkContainer = styled.div`
flex: 1;
margin-right: ${({ theme }) => theme.spacing(2)};
`;
type ApiKeyInputProps = { apiKey: string };
export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => {
const theme = useTheme();
const { enqueueSnackBar } = useSnackBar();
return (
<StyledContainer>
<StyledLinkContainer>
<TextInput value={apiKey} fullWidth />
</StyledLinkContainer>
<Button
Icon={IconCopy}
title="Copy"
onClick={() => {
enqueueSnackBar('Api Key copied to clipboard', {
variant: 'success',
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
});
navigator.clipboard.writeText(apiKey);
}}
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,57 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ApiFieldItem } from '@/settings/developers/types/ApiFieldItem';
import { IconChevronRight } from '@/ui/display/icon';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
export const StyledApisFieldTableRow = styled(TableRow)`
grid-template-columns: 180px 148px 148px 36px;
`;
const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledIconTableCell = styled(TableCell)`
justify-content: center;
padding-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
export const SettingsApiKeysFieldItemTableRow = ({
fieldItem,
onClick,
}: {
fieldItem: ApiFieldItem;
onClick: () => void;
}) => {
const theme = useTheme();
return (
<StyledApisFieldTableRow onClick={() => onClick()}>
<StyledNameTableCell>{fieldItem.name}</StyledNameTableCell>
<TableCell color={theme.font.color.tertiary}>Internal</TableCell>{' '}
<TableCell
color={
fieldItem.expiration === 'Expired'
? theme.font.color.danger
: theme.font.color.tertiary
}
>
{fieldItem.expiration}
</TableCell>
<StyledIconTableCell>
<StyledIconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</StyledIconTableCell>
</StyledApisFieldTableRow>
);
};

View File

@ -0,0 +1,19 @@
import { Meta, StoryObj } from '@storybook/react';
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
const meta: Meta<typeof ApiKeyInput> = {
title: 'Pages/Settings/Developers/ApiKeys/ApiKeyInput',
component: ApiKeyInput,
decorators: [ComponentDecorator, SnackBarDecorator],
args: {
apiKey:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2VudHktN2VkOWQyMTItMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNjk4MTQyODgyLCJleHAiOjE2OTk0MDE1OTksImp0aSI6ImMyMmFiNjQxLTVhOGYtNGQwMC1iMDkzLTk3MzUwYTM2YzZkOSJ9.JIe2TX5IXrdNl3n-kRFp3jyfNUE7unzXZLAzm2Gxl98',
},
};
export default meta;
type Story = StoryObj<typeof ApiKeyInput>;
export const Default: Story = {};

View File

@ -0,0 +1,23 @@
import { Meta, StoryObj } from '@storybook/react';
import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
const meta: Meta<typeof SettingsApiKeysFieldItemTableRow> = {
title: 'Pages/Settings/Developers/ApiKeys/SettingsApiKeysFieldItemTableRow',
component: SettingsApiKeysFieldItemTableRow,
decorators: [ComponentDecorator],
args: {
fieldItem: {
id: '3f4a42e8-b81f-4f8c-9c20-1602e6b34791',
name: 'Zapier Api Key',
type: 'internal',
expiration: 'In 3 days',
},
},
};
export default meta;
type Story = StoryObj<typeof SettingsApiKeysFieldItemTableRow>;
export const Default: Story = {};

View File

@ -0,0 +1,11 @@
export const ExpirationDates: {
value: number | null;
label: string;
}[] = [
{ label: '15 days', value: 15 },
{ label: '30 days', value: 30 },
{ label: '90 days', value: 90 },
{ label: '1 year', value: 365 },
{ label: '2 years', value: 2 * 365 },
{ label: 'Never', value: null },
];

View File

@ -0,0 +1,12 @@
import { useRecoilCallback } from 'recoil';
import { generatedApiKeyFamilyState } from '@/settings/developers/states/generatedApiKeyFamilyState';
export const useGeneratedApiKeys = () => {
return useRecoilCallback(
({ set }) =>
(apiKeyId: string, apiKey: string | null) => {
set(generatedApiKeyFamilyState(apiKeyId), apiKey);
},
);
};

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const generatedApiKeyFamilyState = atomFamily<
string | null | undefined,
string
>({
key: 'generatedApiKeyFamilyState',
default: null,
});

View File

@ -0,0 +1,6 @@
export type ApiFieldItem = {
id: string;
name: string;
type: 'internal' | 'published';
expiration: string;
};

View File

@ -0,0 +1,8 @@
export type ApiKey = {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
name: string;
expiresAt: string;
};

View File

@ -0,0 +1,23 @@
import { computeNewExpirationDate } from '@/settings/developers/utils/compute-new-expiration-date';
jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00.000Z'));
describe('computeNewExpirationDate', () => {
it('should compute properly', () => {
const expiresAt = '2023-01-10T00:00:00.000Z';
const createdAt = '2023-01-01T00:00:00.000Z';
const result = computeNewExpirationDate(expiresAt, createdAt);
expect(result).toEqual('2024-01-10T00:00:00.000Z');
});
it('should compute properly with same values', () => {
const expiresAt = '2023-01-01T10:00:00.000Z';
const createdAt = '2023-01-01T10:00:00.000Z';
const result = computeNewExpirationDate(expiresAt, createdAt);
expect(result).toEqual('2024-01-01T00:00:00.000Z');
});
it('should compute properly with no expiration', () => {
const createdAt = '2023-01-01T10:00:00.000Z';
const result = computeNewExpirationDate(undefined, createdAt);
expect(result).toEqual(null);
});
});

View File

@ -0,0 +1,34 @@
import { formatExpiration } from '@/settings/developers/utils/format-expiration';
jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00.000Z'));
describe('formatExpiration', () => {
it('should format properly when expiresAt is null', () => {
const expiresAt = null;
const result = formatExpiration(expiresAt);
expect(result).toEqual('Never');
const resultWithExpiresMention = formatExpiration(expiresAt, true);
expect(resultWithExpiresMention).toEqual('Never expires');
});
it('should format properly when expiresAt is not null', () => {
const expiresAt = '2024-01-10T00:00:00.000Z';
const result = formatExpiration(expiresAt);
expect(result).toEqual('In 9 days');
const resultWithExpiresMention = formatExpiration(expiresAt, true);
expect(resultWithExpiresMention).toEqual('Expires in 9 days');
});
it('should format properly when expiresAt is large', () => {
const expiresAt = '2034-01-10T00:00:00.000Z';
const result = formatExpiration(expiresAt);
expect(result).toEqual('In 10 years');
const resultWithExpiresMention = formatExpiration(expiresAt, true);
expect(resultWithExpiresMention).toEqual('Expires in 10 years');
});
it('should format properly when expiresAt is large and long version', () => {
const expiresAt = '2034-01-10T00:00:00.000Z';
const result = formatExpiration(expiresAt, undefined, false);
expect(result).toEqual('In 10 years and 9 days');
const resultWithExpiresMention = formatExpiration(expiresAt, true, false);
expect(resultWithExpiresMention).toEqual('Expires in 10 years and 9 days');
});
});

View File

@ -0,0 +1,14 @@
import { DateTime } from 'luxon';
export const computeNewExpirationDate = (
expiresAt: string | null | undefined,
createdAt: string,
): string | null => {
if (!expiresAt) {
return null;
}
const days = DateTime.fromISO(expiresAt).diff(DateTime.fromISO(createdAt), [
'days',
]).days;
return DateTime.utc().plus({ days }).toISO();
};

View File

@ -0,0 +1,31 @@
import { ApiFieldItem } from '@/settings/developers/types/ApiFieldItem';
import { ApiKey } from '@/settings/developers/types/ApiKey';
import { beautifyDateDiff } from '~/utils/date-utils';
export const formatExpiration = (
expiresAt: string | null,
withExpiresMention: boolean = false,
short: boolean = true,
) => {
if (expiresAt) {
const dateDiff = beautifyDateDiff(expiresAt, undefined, short);
if (dateDiff.includes('-')) {
return 'Expired';
}
return withExpiresMention ? `Expires in ${dateDiff}` : `In ${dateDiff}`;
}
return withExpiresMention ? 'Never expires' : 'Never';
};
export const formatExpirations = (
apiKeys: Array<Pick<ApiKey, 'id' | 'name' | 'expiresAt'>>,
): ApiFieldItem[] => {
return apiKeys.map(({ id, name, expiresAt }) => {
return {
id,
name,
expiration: formatExpiration(expiresAt || null),
type: 'internal',
};
});
};