feat(api-key): Add create api key functionality

This commit is contained in:
Myzel394 2023-03-11 12:07:12 +01:00
parent eeb6ba4892
commit dace44f1d9
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
11 changed files with 602 additions and 53 deletions

View File

@ -20,6 +20,7 @@
"@emotion/styled": "^11.10.4",
"@mui/lab": "^5.0.0-alpha.103",
"@mui/material": "^5.10.9",
"@mui/x-date-pickers": "^6.0.1",
"@originjs/vite-plugin-commonjs": "^1.0.3",
"@tanstack/react-query": "^4.12.0",
"axios": "^1.1.2",

View File

@ -59,6 +59,9 @@
},
"report": {
"deleted": "Report has been deleted!"
},
"apiKey": {
"keyCopied": "API key has been copied to your clipboard!"
}
},
"general": {
@ -89,5 +92,22 @@
"signup": "Sign up",
"login": "Log in",
"logout": "Log out"
},
"values": {
"scopes": {
"basic_profile": "Basic Profile",
"full_profile": "Full Profile",
"read_preferences": "Read Preferences",
"update_preferences": "Update Preferences",
"read_alias": "Read Aliases",
"create_alias": "Create Aliases",
"update_alias": "Update Aliases",
"delete_alias": "Delete Aliases",
"read_report": "Read Reports",
"delete_report": "Delete Reports"
}
}
}

View File

@ -1,7 +1,32 @@
{
"title": "Manage your API Keys",
"create": {
"label": "Create a new API Key"
"label": "Create a new API Key",
"description": "Define a label and the scopes you want to grant to this API Key.",
"continueActionLabel": "Create API Key",
"success": "Your API Key has been created. Copy it now, you won't be able to see it again.",
"form": {
"label": {
"label": "Label"
},
"expiresAt": {
"label": "Expiration date",
"errors": {
"tooFarInFuture": "This date is too far in the future.",
"inPast": "Expiration date must be in the future."
},
"values": {
"1days": "1 Day",
"7days": "1 Week",
"30days": "1 Month",
"180days": "1/2 Year",
"360days": "1 Year"
}
},
"scopes": {
"label": "Scopes"
}
}
},
"emptyState": {
"title": "Welcome to your API Keys",

View File

@ -7,6 +7,8 @@ import React, {ReactElement} from "react"
import {queryClient} from "~/constants/react-query"
import {getServerSettings} from "~/apis"
import {darkTheme, lightTheme} from "~/constants/themes"
import {LocalizationProvider} from "@mui/x-date-pickers"
import {AdapterDateFns} from "@mui/x-date-pickers/AdapterDateFns"
import AdminRoute from "~/routes/AdminRoute"
import AliasDetailRoute from "~/routes/AliasDetailRoute"
import AliasesRoute from "~/routes/AliasesRoute"
@ -108,6 +110,7 @@ const router = createBrowserRouter([
},
{
path: "/settings/api-keys",
loader: getServerSettings,
element: <SettingsAPIKeysRoute />,
},
{
@ -167,9 +170,11 @@ export default function App(): ReactElement {
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={THEME_THEME_MAP[theme]}>
<SnackbarProvider>
<CssBaseline />
<RouterProvider router={router} />
<I18nHandler />
<LocalizationProvider dateAdapter={AdapterDateFns}>
<CssBaseline />
<RouterProvider router={router} />
<I18nHandler />
</LocalizationProvider>
</SnackbarProvider>
</ThemeProvider>
</QueryClientProvider>

View File

@ -1,5 +1,6 @@
import {APIKey} from "~/server-types"
import {client} from "~/constants/axios-client"
import formatISO from "date-fns/formatISO"
import parseAPIKey from "~/apis/helpers/parse-api-key"
export interface CreateAPIKeyData {
@ -13,12 +14,22 @@ export default async function createAPIKey({
label,
scopes,
expiresAt,
}: CreateAPIKeyData): Promise<APIKey> {
const {data} = await client.post(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/api-key`, {
label,
scopes,
expiresAt,
})
}: CreateAPIKeyData): Promise<APIKey & {key: string}> {
const {data} = await client.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/api-key`,
{
label,
scopes,
expiresAt: expiresAt
? formatISO(expiresAt, {
representation: "date",
})
: undefined,
},
{
withCredentials: true,
},
)
return parseAPIKey(data)
return parseAPIKey(data) as APIKey & {key: string}
}

View File

@ -16,3 +16,18 @@ export const DEFAULT_ALIAS_NOTE: AliasNote = {
export const ERROR_SNACKBAR_SHOW_DURATION = 5000
export const SUCCESS_SNACKBAR_SHOW_DURATION = 2000
export const AUTHENTICATION_PATHS = ["/auth/login", "/auth/signup", "/auth/complete-account"]
export const API_KEY_SCOPES = [
"basic_profile",
"full_profile",
"read:preferences",
"update:preferences",
"read:alias",
"create:alias",
"update:alias",
"delete:alias",
"read:report",
"delete:report",
]

View File

@ -1,24 +1,53 @@
import {ReactElement} from "react"
import {APIKey} from "~/server-types"
import {IconButton, ListItem, ListItemSecondaryAction, ListItemText} from "@mui/material"
import {Alert, IconButton, ListItem, ListItemSecondaryAction, ListItemText} from "@mui/material"
import {useTranslation} from "react-i18next"
import {MdDelete} from "react-icons/md"
import {MdContentCopy, MdDelete} from "react-icons/md"
import {useCopyToClipboard} from "react-use"
import {ErrorSnack, SuccessSnack} from "~/components"
export interface APIKeyListItemProps {
apiKey: APIKey
privateKey?: string
}
export default function APIKeyListItem({apiKey}: APIKeyListItemProps): ReactElement {
const {t} = useTranslation("settings-api-keys")
export default function APIKeyListItem({apiKey, privateKey}: APIKeyListItemProps): ReactElement {
const {t} = useTranslation(["settings-api-keys", "common"])
const [{value, error}, copy] = useCopyToClipboard()
if (privateKey) {
return (
<>
<Alert severity="success">
<ListItem>
<ListItemText>{privateKey}</ListItemText>
<ListItemSecondaryAction>
<IconButton edge="end" onClick={() => copy(privateKey)}>
<MdContentCopy />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</Alert>
<SuccessSnack
key={value}
message={value && t("messages.apiKey.keyCopied", {ns: "common"})}
/>
<ErrorSnack message={error && t("messages.errors.copyFailed", {ns: "common"})} />
</>
)
}
return (
<ListItem>
<ListItemText primary={apiKey.label} secondary={apiKey.expiresAt.toString()} />
<ListItemSecondaryAction>
<IconButton edge="end">
<MdDelete />
</IconButton>
</ListItemSecondaryAction>
<>
<ListItemText primary={apiKey.label} secondary={apiKey.expiresAt.toString()} />
<ListItemSecondaryAction>
<IconButton edge="end">
<MdDelete />
</IconButton>
</ListItemSecondaryAction>
</>
</ListItem>
)
}

View File

@ -0,0 +1,311 @@
import * as yup from "yup"
import {ReactElement} from "react"
import {
Alert,
Badge,
Box,
Button,
Chip,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
FormControl,
FormHelperText,
Grid,
InputAdornment,
InputLabel,
ListItem,
ListItemIcon,
ListItemText,
MenuItem,
Select,
TextField,
} from "@mui/material"
import {useTranslation} from "react-i18next"
import {useFormik} from "formik"
import {CreateAPIKeyData, createAPIKey} from "~/apis"
import {APIKey, APIKeyScope, ServerSettings} from "~/server-types"
import {BiText} from "react-icons/bi"
import {CgProfile} from "react-icons/cg"
import {DatePicker} from "@mui/x-date-pickers"
import {useLoaderData} from "react-router-dom"
import {API_KEY_SCOPES} from "~/constants/values"
import {FaMask} from "react-icons/fa"
import {MdAdd, MdCancel, MdDelete, MdEdit, MdTextSnippet} from "react-icons/md"
import {GoSettings} from "react-icons/go"
import {TiEye} from "react-icons/ti"
import {useMutation} from "@tanstack/react-query"
import {AxiosError} from "axios"
import {parseFastAPIError} from "~/utils"
import {useErrorSuccessSnacks} from "~/hooks"
import addDays from "date-fns/addDays"
import diffInDays from "date-fns/differenceInDays"
import set from "date-fns/set"
export interface CreateNewAPIKeyDialogProps {
open: boolean
onClose: () => void
onCreated: (key: APIKey & {key: string}) => void
}
const PRESET_DAYS: number[] = [1, 7, 30, 180, 360]
const API_KEY_SCOPE_ICON_MAP: Record<string, ReactElement> = {
profile: <CgProfile />,
alias: <FaMask />,
report: <MdTextSnippet />,
preferences: <GoSettings />,
}
const API_KEY_SCOPE_TYPE_ICON_MAP: Record<string, ReactElement> = {
read: <TiEye />,
create: <MdAdd />,
update: <MdEdit />,
delete: <MdDelete />,
}
const normalizeTime = (date: Date) =>
set(date, {
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
})
export default function CreateNewAPIKeyDialog({
open,
onClose,
onCreated,
}: CreateNewAPIKeyDialogProps): ReactElement {
const {t} = useTranslation(["settings-api-keys", "common"])
const serverSettings = useLoaderData() as ServerSettings
const {showSuccess} = useErrorSuccessSnacks()
const scheme = yup.object().shape({
label: yup.string().required().label(t("create.form.label.label")),
expiresAt: yup.date().required().label(t("create.form.expiresAt.label")),
scopes: yup
.array<APIKeyScope[]>()
.of(yup.string())
.required()
.label(t("create.form.scopes.label")),
})
const {mutateAsync} = useMutation<APIKey & {key: string}, AxiosError, CreateAPIKeyData>(
data => createAPIKey(data),
{
onSuccess: async key => {
onClose()
showSuccess(t("create.success"))
onCreated(key)
},
},
)
const formik = useFormik<CreateAPIKeyData & {detail: string}>({
validationSchema: scheme,
initialValues: {
label: "",
expiresAt: addDays(new Date(), 30),
scopes: [],
detail: "",
},
onSubmit: async (values, {setErrors}) => {
try {
await mutateAsync(values)
} catch (error) {
setErrors(parseFastAPIError(error as AxiosError))
}
},
})
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t("create.label")}</DialogTitle>
<form onSubmit={formik.handleSubmit}>
<DialogContent>
<DialogContentText>{t("create.description")}</DialogContentText>
<Grid container spacing={4} mt={1}>
<Grid item xs={12}>
<TextField
fullWidth
name="label"
id="label"
label={t("create.form.label.label")}
disabled={formik.isSubmitting}
error={formik.touched.label && Boolean(formik.errors.label)}
helperText={formik.touched.label && formik.errors.label}
value={formik.values.label}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<BiText />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<FormControl
fullWidth
error={formik.touched.scopes && Boolean(formik.errors.scopes)}
>
<InputLabel htmlFor="scopes" id="scopes-label">
{t("create.form.scopes.label")}
</InputLabel>
<Select<string[]>
multiple
fullWidth
name="scopes"
id="scopes"
label={t("create.form.scopes.label")}
disabled={formik.isSubmitting}
error={formik.touched.scopes && Boolean(formik.errors.scopes)}
value={formik.values.scopes}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
renderValue={(selected: string[]) => (
<Box sx={{display: "flex", flexWrap: "wrap", gap: 0.5}}>
{selected.map(value => (
<Chip
key={value}
label={t(
`values.scopes.${value.replace(":", "_")}`,
{
ns: "common",
},
)}
/>
))}
</Box>
)}
>
{API_KEY_SCOPES.map(scope => (
<MenuItem key={scope} value={scope}>
<ListItem>
<ListItemIcon>
<Badge
badgeContent={
API_KEY_SCOPE_TYPE_ICON_MAP[
scope
.replace(":", "_")
.split("_")[0]
]
}
>
{
API_KEY_SCOPE_ICON_MAP[
scope
.replace(":", "_")
.split("_")[1]
]
}
</Badge>
</ListItemIcon>
<ListItemText>
{t(`values.scopes.${scope.replace(":", "_")}`, {
ns: "common",
})}
</ListItemText>
</ListItem>
</MenuItem>
))}
</Select>
<FormHelperText
error={Boolean(formik.touched.scopes && formik.errors.scopes)}
>
{formik.touched.scopes && formik.errors.scopes}
</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={12}>
<Grid container spacing={1}>
<Grid item xs={12}>
<DatePicker
value={formik.values.expiresAt}
disabled={formik.isSubmitting}
label={t("create.form.expiresAt.label")}
slotProps={{
textField: {
fullWidth: true,
name: "expiresAt",
error:
formik.touched.expiresAt &&
Boolean(formik.errors.expiresAt),
helperText:
formik.touched.expiresAt &&
formik.errors.expiresAt,
onBlur: formik.handleBlur,
},
}}
minDate={new Date()}
maxDate={addDays(new Date(), serverSettings.apiKeyMaxDays)}
onChange={value => formik.setFieldValue("expiresAt", value)}
/>
<FormHelperText
error={
formik.touched.expiresAt &&
Boolean(formik.errors.expiresAt)
}
>
{formik.touched.expiresAt && formik.errors.expiresAt}
</FormHelperText>
</Grid>
<Grid item>
<Grid container spacing={1} direction="row">
{PRESET_DAYS.filter(
days => days <= serverSettings.apiKeyMaxDays,
).map(days => (
<Grid item key={days}>
<Chip
label={t(
`create.form.expiresAt.values.${days}days`,
)}
variant={
diffInDays(
formik.values.expiresAt!,
normalizeTime(new Date()),
) >= days
? "filled"
: "outlined"
}
onClick={() =>
formik.setFieldValue(
"expiresAt",
addDays(
normalizeTime(new Date()),
days,
),
)
}
/>
</Grid>
))}
</Grid>
</Grid>
</Grid>
</Grid>
{formik.errors.detail && (
<Grid item xs={12}>
<Alert severity="error">{formik.errors.detail}</Alert>
</Grid>
)}
</Grid>
</DialogContent>
<DialogActions>
<Button startIcon={<MdCancel />} onClick={onClose}>
{t("general.cancelLabel", {ns: "common"})}
</Button>
<Button type="submit" variant="contained">
{t("create.continueActionLabel")}
</Button>
</DialogActions>
</form>
</Dialog>
)
}

View File

@ -1,4 +1,4 @@
import {ReactElement} from "react"
import {ReactElement, useState} from "react"
import {useTranslation} from "react-i18next"
import {useQuery} from "@tanstack/react-query"
import {APIKey, PaginationResult} from "~/server-types"
@ -6,43 +6,65 @@ import {AxiosError} from "axios"
import {getAPIKeys} from "~/apis"
import {QueryResult, SimplePage} from "~/components"
import {Button, List} from "@mui/material"
import {Link} from "react-router-dom"
import {MdAdd} from "react-icons/md"
import APIKeyListItem from "~/route-widgets/SettingsAPIKeysRoute/APIKeyListItem"
import CreateNewAPIKeyDialog from "../route-widgets/SettingsAPIKeysRoute/CreateNewAPIKeyDialog"
import EmptyStateScreen from "~/route-widgets/SettingsAPIKeysRoute/EmptyStateScreen"
export default function SettingsAPIKeysRoute(): ReactElement {
const {t} = useTranslation("settings-api-keys")
const query = useQuery<PaginationResult<APIKey>, AxiosError>(["get_api_keys"], () =>
getAPIKeys(),
)
const queryKey = ["get_api_keys"]
const query = useQuery<PaginationResult<APIKey>, AxiosError>(queryKey, () => getAPIKeys())
const [createdAPIKey, setCreatedAPIKey] = useState<(APIKey & {key: string}) | null>(null)
const [createNew, setCreateNew] = useState<boolean>(false)
return (
<SimplePage
title={t("title")}
actions={
<Button
variant="contained"
color="primary"
component={Link}
to="/settings/api-keys/new"
>
{t("create.label")}
</Button>
}
>
<QueryResult<PaginationResult<APIKey>, AxiosError> query={query}>
{({items: apiKeys}) =>
apiKeys.length > 0 ? (
<List>
{apiKeys.map(apiKey => (
<APIKeyListItem apiKey={apiKey} key={apiKey.id} />
))}
</List>
) : (
<EmptyStateScreen />
)
<>
<SimplePage
title={t("title")}
actions={
<Button
variant="contained"
color="primary"
onClick={() => setCreateNew(true)}
startIcon={<MdAdd />}
>
{t("create.label")}
</Button>
}
</QueryResult>
</SimplePage>
>
<QueryResult<PaginationResult<APIKey>, AxiosError> query={query}>
{({items: apiKeys}) =>
apiKeys.length > 0 ? (
<List>
{apiKeys.map(apiKey => (
<APIKeyListItem
apiKey={apiKey}
key={apiKey.id}
privateKey={
apiKey.id === createdAPIKey?.id
? createdAPIKey.key
: undefined
}
/>
))}
</List>
) : (
<EmptyStateScreen />
)
}
</QueryResult>
</SimplePage>
<CreateNewAPIKeyDialog
key={createNew.toString()}
open={createNew}
onClose={() => setCreateNew(false)}
onCreated={key => {
query.refetch()
setCreatedAPIKey(key)
}}
/>
</>
)
}

View File

@ -84,6 +84,7 @@ export interface ServerSettings {
instanceSalt: string
publicKey: string
allowAliasDeletion: boolean
apiKeyMaxDays: number
}
export interface Alias {
@ -141,11 +142,23 @@ export interface AliasList {
type: AliasType
}
export type APIKeyScope =
| "profile_basic"
| "full_profile"
| "read:preferences"
| "update:preferences"
| "read:alias"
| "create:alias"
| "update:alias"
| "delete:alias"
| "read:report"
| "delete:report"
export interface APIKey {
id: string
label: string
expiresAt: Date
scopes: string[]
scopes: APIKeyScope[]
}
export interface Report {

View File

@ -464,6 +464,13 @@
dependencies:
regenerator-runtime "^0.13.10"
"@babel/runtime@^7.20.13", "@babel/runtime@^7.21.0":
version "7.21.0"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/template@^7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@ -562,6 +569,60 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@date-io/core@^2.16.0":
version "2.16.0"
resolved "https://registry.npmjs.org/@date-io/core/-/core-2.16.0.tgz#7871bfc1d9bca9aa35ad444a239505589d0f22f6"
integrity sha512-DYmSzkr+jToahwWrsiRA2/pzMEtz9Bq1euJwoOuYwuwIYXnZFtHajY2E6a1VNVDc9jP8YUXK1BvnZH9mmT19Zg==
"@date-io/date-fns-jalali@^2.16.0":
version "2.16.0"
resolved "https://registry.npmjs.org/@date-io/date-fns-jalali/-/date-fns-jalali-2.16.0.tgz#0e2916c287e00d708a6f9f6d6e6c44ec377994c5"
integrity sha512-MNVvGYwRiBydbvY7gvZM14W2kosIG29G1Ekw5qmYWOXkIIFngh6ZvV7/uVGDCW+gqlIeSz/XitZXA9n8RO0tJw==
dependencies:
"@date-io/core" "^2.16.0"
"@date-io/date-fns@^2.16.0":
version "2.16.0"
resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.16.0.tgz#bd5e09b6ecb47ee55e593fc3a87e7b2caaa3da40"
integrity sha512-bfm5FJjucqlrnQcXDVU5RD+nlGmL3iWgkHTq3uAZWVIuBu6dDmGa3m8a6zo2VQQpu8ambq9H22UyUpn7590joA==
dependencies:
"@date-io/core" "^2.16.0"
"@date-io/dayjs@^2.16.0":
version "2.16.0"
resolved "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.16.0.tgz#0d2c254ad8db1306fdc4b8eda197cb53c9af89dc"
integrity sha512-y5qKyX2j/HG3zMvIxTobYZRGnd1FUW2olZLS0vTj7bEkBQkjd2RO7/FEwDY03Z1geVGlXKnzIATEVBVaGzV4Iw==
dependencies:
"@date-io/core" "^2.16.0"
"@date-io/hijri@^2.16.1":
version "2.16.1"
resolved "https://registry.npmjs.org/@date-io/hijri/-/hijri-2.16.1.tgz#a5e7e5b875e0ac8719c235eaf51d4188f21ea193"
integrity sha512-6BxY0mtnqj5cBiXluRs3uWN0mSJwGw0AB2ZxqtEHvBFoiSYEojW51AETnfPIWpdvDsBn+WAC7QrfBvQZnoyIkQ==
dependencies:
"@date-io/moment" "^2.16.1"
"@date-io/jalaali@^2.16.1":
version "2.16.1"
resolved "https://registry.npmjs.org/@date-io/jalaali/-/jalaali-2.16.1.tgz#c53323fc429f8fe6ab205d36c003d6de071f17e8"
integrity sha512-GLw87G/WJ1DNrQHW8p/LqkqAqTUSqBSRin0H1pRPwCccB5Fh7GT64sadjzEvjW56lPJ0aq2vp5yI2eIjZajfrw==
dependencies:
"@date-io/moment" "^2.16.1"
"@date-io/luxon@^2.16.1":
version "2.16.1"
resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.16.1.tgz#b08786614cb58831c729a15807753011e4acb966"
integrity sha512-aeYp5K9PSHV28946pC+9UKUi/xMMYoaGelrpDibZSgHu2VWHXrr7zWLEr+pMPThSs5vt8Ei365PO+84pCm37WQ==
dependencies:
"@date-io/core" "^2.16.0"
"@date-io/moment@^2.16.1":
version "2.16.1"
resolved "https://registry.npmjs.org/@date-io/moment/-/moment-2.16.1.tgz#ec6e0daa486871e0e6412036c6f806842a0eeed4"
integrity sha512-JkxldQxUqZBfZtsaCcCMkm/dmytdyq5pS1RxshCQ4fHhsvP5A7gSqPD22QbVXMcJydi3d3v1Y8BQdUKEuGACZQ==
dependencies:
"@date-io/core" "^2.16.0"
"@emotion/babel-plugin@^11.10.0":
version "11.10.2"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz#879db80ba622b3f6076917a1e6f648b1c7d008c7"
@ -1077,6 +1138,37 @@
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/utils@^5.11.7":
version "5.11.12"
resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.11.12.tgz#627f491c0e7267398590af5e6cb14b306170d914"
integrity sha512-5vH9B/v8pzkpEPO2HvGM54ToXV6cFdAn8UrvdN8TMEEwpn/ycW0jLiyBcgUlPsQ+xha7hqXCPQYHaYFDIcwaiw==
dependencies:
"@babel/runtime" "^7.21.0"
"@types/prop-types" "^15.7.5"
"@types/react-is" "^16.7.1 || ^17.0.0"
prop-types "^15.8.1"
react-is "^18.2.0"
"@mui/x-date-pickers@^6.0.1":
version "6.0.1"
resolved "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.0.1.tgz#855f71b0661602c105dfd75bd0d38cd8c40d59f5"
integrity sha512-eZ3uTWp2uA08h4IULoqI7rkP16ltzZH0kSdZdvLCvaa4ix1ZP5zGcvt8CdtxM+NFWjRS7fGWPOgOoScGw9lKLQ==
dependencies:
"@babel/runtime" "^7.20.13"
"@date-io/core" "^2.16.0"
"@date-io/date-fns" "^2.16.0"
"@date-io/date-fns-jalali" "^2.16.0"
"@date-io/dayjs" "^2.16.0"
"@date-io/hijri" "^2.16.1"
"@date-io/jalaali" "^2.16.1"
"@date-io/luxon" "^2.16.1"
"@date-io/moment" "^2.16.1"
"@mui/utils" "^5.11.7"
"@types/react-transition-group" "^4.4.5"
clsx "^1.2.1"
prop-types "^15.8.1"
react-transition-group "^4.4.5"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -4774,6 +4866,11 @@ regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee"
integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==
regenerator-runtime@^0.13.11:
version "0.13.11"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regexp.prototype.flags@^1.3.0, regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"