[P-UI] Home page (#5344)

* Add new start page

* [FR/P-UI] Home/Start page - widgets

* load widgets dynamic

* code cleanup

* remove lodash

* refactor and rename to WidgetLayout

* Add CSS serving

* removed unneeded complexity

* clean up UI; switch to menu for controls

* change signature

* added hotkey

* removed hover

* removed dummy widget

* Add translations

* fix test

* uses real data for getting started

* adapted style for GettingStartedCard

* added placeholder usage to GettingStartedCard
This commit is contained in:
Matthias Mair 2023-08-09 14:32:53 +02:00 committed by GitHub
parent 62362455b8
commit ebbc27047b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 3301 additions and 729 deletions

View File

@ -64,7 +64,7 @@ class AuthRequiredMiddleware(object):
elif request.path_info.startswith('/accounts/'):
authorized = True
elif request.path_info.startswith('/platform/') or request.path_info == '/platform':
elif request.path_info.startswith('/platform/') or request.path_info.startswith('/assets/') or request.path_info == '/platform':
authorized = True
elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():

View File

@ -34,7 +34,6 @@ from report.api import report_api_urls
from stock.api import stock_api_urls
from stock.urls import stock_urls
from users.api import user_urls
from web.urls import spa_view
from web.urls import urlpatterns as platform_urls
from .api import APISearchView, InfoView, NotFoundView
@ -212,11 +211,7 @@ classic_frontendpatterns = [
]
new_frontendpatterns = [
# Platform urls
re_path(r'^platform/', include(platform_urls)),
re_path(r'^platform', spa_view, name='platform'),
]
new_frontendpatterns = platform_urls
# Load patterns for frontend according to settings
frontendpatterns = []

View File

@ -22,6 +22,7 @@ def spa_bundle():
manifest_data = json.load(manifest.open())
index = manifest_data.get("index.html")
css_index = manifest_data.get("index.css")
dynmanic_files = index.get("dynamicImports", [])
imports_files = "".join(
@ -32,5 +33,6 @@ def spa_bundle():
)
return mark_safe(
f"""<script type="module" src="{settings.STATIC_URL}web/{index['file']}"></script>{imports_files}"""
f"""<link rel="stylesheet" href="{settings.STATIC_URL}web/{css_index['file']}" />
<script type="module" src="{settings.STATIC_URL}web/{index['file']}"></script>{imports_files}"""
)

View File

@ -1,7 +1,7 @@
"""URLs for web app."""
from django.conf import settings
from django.shortcuts import redirect
from django.urls import path, re_path
from django.urls import include, path, re_path
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import TemplateView
@ -20,8 +20,12 @@ spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name="web/index.html
urlpatterns = [
path('platform/', include([
path("assets/<path:path>", RedirectAssetView.as_view()),
re_path(r"^(?P<path>.*)/$", spa_view),
path("set-password?uid=<uid>&token=<token>", spa_view, name="password_reset_confirm"),
path("", spa_view),]
)),
re_path(r'^platform', spa_view, name='platform'),
path("assets/<path:path>", RedirectAssetView.as_view()),
re_path(r"^(?P<path>.*)/$", spa_view),
path("set-password?uid=<uid>&token=<token>", spa_view, name="password_reset_confirm"),
path("", spa_view),
]

View File

@ -18,6 +18,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@lingui/core": "^4.3.0",
"@lingui/react": "^4.3.0",
"@mantine/carousel": "^6.0.17",
"@mantine/core": "^6.0.17",
"@mantine/dates": "^6.0.17",
"@mantine/dropzone": "^6.0.17",
@ -29,10 +30,12 @@
"@tanstack/react-query": "^4.32.0",
"axios": "^1.4.0",
"dayjs": "^1.11.9",
"embla-carousel-react": "^8.0.0-rc11",
"html5-qrcode": "^2.3.8",
"mantine-datatable": "^2.9.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-grid-layout": "^1.3.4",
"react-router-dom": "^6.14.2",
"zustand": "^4.3.9"
},
@ -46,6 +49,7 @@
"@types/node": "^20.4.4",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-grid-layout": "^1.3.2",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.0.3",
"babel-plugin-macros": "^3.1.0",

View File

@ -0,0 +1,92 @@
import { Trans } from '@lingui/macro';
import { Carousel } from '@mantine/carousel';
import {
Anchor,
Button,
Paper,
Text,
Title,
createStyles,
rem
} from '@mantine/core';
import { DocumentationLinkItem } from './DocumentationLinks';
import { PlaceholderPill } from './Placeholder';
const useStyles = createStyles((theme) => ({
card: {
height: rem(170),
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'flex-start',
backgroundSize: 'cover',
backgroundPosition: 'center'
},
title: {
fontWeight: 900,
color:
theme.colorScheme === 'dark' ? theme.colors.white : theme.colors.dark,
lineHeight: 1.2,
fontSize: rem(32),
marginTop: 0
},
category: {
color:
theme.colorScheme === 'dark' ? theme.colors.white : theme.colors.dark,
opacity: 0.7,
fontWeight: 700
}
}));
function StartedCard({
title,
description,
link,
placeholder
}: DocumentationLinkItem) {
const { classes } = useStyles();
return (
<Paper shadow="md" p="xl" radius="md" className={classes.card}>
<div>
<Title order={3} className={classes.title}>
{title} {placeholder && <PlaceholderPill />}
</Title>
<Text size="sm" className={classes.category} lineClamp={2}>
{description}
</Text>
</div>
<Anchor href={link} target="_blank">
<Button>
<Trans>Read more</Trans>
</Button>
</Anchor>
</Paper>
);
}
export function GettingStartedCarousel({
items
}: {
items: DocumentationLinkItem[];
}) {
const slides = items.map((item) => (
<Carousel.Slide key={item.id}>
<StartedCard {...item} />
</Carousel.Slide>
));
return (
<Carousel
slideSize="50%"
breakpoints={[{ maxWidth: 'sm', slideSize: '100%', slideGap: rem(2) }]}
slideGap="xl"
align="start"
>
{slides}
</Carousel>
);
}

View File

@ -0,0 +1,29 @@
import { Trans } from '@lingui/macro';
import { SimpleGrid, Title } from '@mantine/core';
import { ColorToggle } from '../items/ColorToggle';
import { LanguageSelect } from '../items/LanguageSelect';
export default function DisplayWidget() {
return (
<span>
<Title order={5}>
<Trans>Display Settings</Trans>
</Title>
<SimpleGrid cols={2} spacing={0}>
<div>
<Trans>Color Mode</Trans>
</div>
<div>
<ColorToggle />
</div>
<div>
<Trans>Language</Trans>
</div>
<div>
<LanguageSelect />
</div>
</SimpleGrid>
</span>
);
}

View File

@ -0,0 +1,35 @@
import { Trans } from '@lingui/macro';
import { Button, Stack, Title } from '@mantine/core';
import { IconExternalLink } from '@tabler/icons-react';
export default function FeedbackWidget() {
return (
<Stack
sx={(theme) => ({
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.gray[9]
: theme.colors.gray[1],
borderRadius: theme.radius.md
})}
p={15}
>
<Title order={5}>
<Trans>Something is new: Platform UI</Trans>
</Title>
<Trans>
We are building a new UI with a modern stack. What you currently see is
not fixed and will be redesigned but demonstrates the UI/UX
possibilities we will have going forward.
</Trans>
<Button
component="a"
href="https://github.com/inventree/InvenTree/discussions/5328"
variant="outline"
leftIcon={<IconExternalLink size="0.9rem" />}
>
<Trans>Provide Feedback</Trans>
</Button>
</Stack>
);
}

View File

@ -0,0 +1,16 @@
import { Trans } from '@lingui/macro';
import { Title } from '@mantine/core';
import { navDocLinks } from '../../defaults/links';
import { GettingStartedCarousel } from '../items/GettingStartedCarousel';
export default function GetStartedWidget() {
return (
<span>
<Title order={5}>
<Trans>Getting started</Trans>
</Title>
<GettingStartedCarousel items={navDocLinks} />
</span>
);
}

View File

@ -0,0 +1,246 @@
import { Trans } from '@lingui/macro';
import {
ActionIcon,
Container,
Group,
Indicator,
createStyles
} from '@mantine/core';
import { Menu, Text } from '@mantine/core';
import { useDisclosure, useHotkeys } from '@mantine/hooks';
import {
IconArrowBackUpDouble,
IconDotsVertical,
IconLayout2,
IconSquare,
IconSquareCheck
} from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { Responsive, WidthProvider } from 'react-grid-layout';
const ReactGridLayout = WidthProvider(Responsive);
interface LayoutStorage {
[key: string]: {};
}
const compactType = 'vertical';
const useItemStyle = createStyles((theme) => ({
backgroundItem: {
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.white,
maxWidth: '100%',
padding: '8px',
boxShadow: theme.shadows.md
},
baseItem: {
maxWidth: '100%',
padding: '8px'
}
}));
export interface LayoutItemType {
i: number;
val: string | JSX.Element | JSX.Element[] | (() => JSX.Element);
w?: number;
h?: number;
x?: number;
y?: number;
minH?: number;
}
export function WidgetLayout({
items = [],
className = 'layout',
localstorageName = 'argl',
rowHeight = 30
}: {
items: LayoutItemType[];
className?: string;
localstorageName?: string;
rowHeight?: number;
}) {
const [layouts, setLayouts] = useState({});
const [editable, setEditable] = useDisclosure(false);
const [boxShown, setBoxShown] = useDisclosure(true);
const { classes } = useItemStyle();
useEffect(() => {
let layout = getFromLS('layouts') || [];
const new_layout = JSON.parse(JSON.stringify(layout));
setLayouts(new_layout);
}, []);
function getFromLS(key: string) {
let ls: LayoutStorage = {};
if (localStorage) {
try {
ls = JSON.parse(localStorage.getItem(localstorageName) || '') || {};
} catch (e) {
/*Ignore*/
}
}
return ls[key];
}
function saveToLS(key: string, value: any) {
if (localStorage) {
localStorage.setItem(
localstorageName,
JSON.stringify({
[key]: value
})
);
}
}
function resetLayout() {
setLayouts({});
}
function onLayoutChange(layout: any, layouts: any) {
saveToLS('layouts', layouts);
setLayouts(layouts);
}
return (
<div>
<WidgetControlBar
editable={editable}
editFnc={setEditable.toggle}
resetLayout={resetLayout}
boxShown={boxShown}
boxFnc={setBoxShown.toggle}
/>
{layouts ? (
<ReactGridLayout
className={className}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
rowHeight={rowHeight}
layouts={layouts}
onLayoutChange={(layout, layouts) => onLayoutChange(layout, layouts)}
compactType={compactType}
isDraggable={editable}
isResizable={editable}
>
{items.map((item) => {
return LayoutItem(item, boxShown, classes);
})}
</ReactGridLayout>
) : (
<div>
<Trans>Loading</Trans>
</div>
)}
</div>
);
}
function WidgetControlBar({
editable,
editFnc,
resetLayout,
boxShown,
boxFnc
}: {
editable: boolean;
editFnc: () => void;
resetLayout: () => void;
boxShown: boolean;
boxFnc: () => void;
}) {
useHotkeys([['mod+E', () => editFnc()]]);
return (
<Group position="right">
<Menu
shadow="md"
width={200}
openDelay={100}
closeDelay={400}
position="bottom-end"
>
<Menu.Target>
<Indicator
color="red"
position="bottom-start"
processing
disabled={!editable}
>
<ActionIcon variant="transparent">
<IconDotsVertical />
</ActionIcon>
</Indicator>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>
<Trans>Layout</Trans>
</Menu.Label>
<Menu.Item
icon={<IconArrowBackUpDouble size={14} />}
onClick={resetLayout}
>
<Trans>Reset Layout</Trans>
</Menu.Item>
<Menu.Item
icon={
<IconLayout2 size={14} color={editable ? 'red' : undefined} />
}
onClick={editFnc}
rightSection={
<Text size="xs" color="dimmed">
E
</Text>
}
>
{editable ? <Trans>Stop Edit</Trans> : <Trans>Edit Layout</Trans>}
</Menu.Item>
<Menu.Divider />
<Menu.Label>
<Trans>Appearance</Trans>
</Menu.Label>
<Menu.Item
icon={
boxShown ? (
<IconSquareCheck size={14} />
) : (
<IconSquare size={14} />
)
}
onClick={boxFnc}
>
<Trans>Show Boxes</Trans>
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
);
}
function LayoutItem(
item: any,
backgroundColor: boolean,
classes: { backgroundItem: string; baseItem: string }
) {
return (
<Container
key={item.i}
data-grid={{
w: item.w || 3,
h: item.h || 3,
x: item.x || 0,
y: item.y || 0,
minH: item.minH || undefined,
minW: item.minW || undefined
}}
className={backgroundColor ? classes.backgroundItem : classes.baseItem}
>
{item.val}
</Container>
);
}

View File

@ -17,3 +17,8 @@ export const Loadable = (Component: any) => (props: JSX.IntrinsicAttributes) =>
<Component {...props} />
</Suspense>
);
export function LoadingItem({ item }: { item: any }): JSX.Element {
const Itm = Loadable(item);
return <Itm />;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import App from './App';

View File

@ -1,18 +1,63 @@
import { Trans } from '@lingui/macro';
import { Group } from '@mantine/core';
import { Title } from '@mantine/core';
import { lazy } from 'react';
import { PlaceholderPill } from '../../components/items/Placeholder';
import { StylishText } from '../../components/items/StylishText';
import {
LayoutItemType,
WidgetLayout
} from '../../components/widgets/WidgetLayout';
import { LoadingItem } from '../../functions/loading';
import { useApiState } from '../../states/ApiState';
const vals: LayoutItemType[] = [
{
i: 1,
val: (
<LoadingItem
item={lazy(() => import('../../components/widgets/GetStartedWidget'))}
/>
),
w: 12,
h: 6,
x: 0,
y: 0,
minH: 6
},
{
i: 2,
val: (
<LoadingItem
item={lazy(() => import('../../components/widgets/DisplayWidget'))}
/>
),
w: 3,
h: 3,
x: 0,
y: 7,
minH: 3
},
{
i: 4,
val: (
<LoadingItem
item={lazy(() => import('../../components/widgets/FeedbackWidget'))}
/>
),
w: 4,
h: 6,
x: 0,
y: 9
}
];
export default function Home() {
const [username] = useApiState((state) => [state.user?.name]);
return (
<>
<Group>
<StylishText>
<Trans>Home</Trans>
</StylishText>
<PlaceholderPill />
</Group>
<Title order={1}>
<Trans>Welcome to your Dashboard{username && `, ${username}`}</Trans>
</Title>
<WidgetLayout items={vals} />
</>
);
}

View File

@ -11,5 +11,9 @@ test('Basic Platform UI test', async ({ page }) => {
await page.goto('./platform/');
await expect(page).toHaveTitle('InvenTree Demo Server');
await expect(page.getByText('Home').nth(1)).toBeVisible();
await expect(
page.getByRole('heading', {
name: 'Welcome to your Dashboard, Ally Access'
})
).toBeVisible();
});

View File

@ -877,6 +877,13 @@
"@babel/runtime" "^7.20.13"
"@lingui/core" "4.3.0"
"@mantine/carousel@^6.0.17":
version "6.0.17"
resolved "https://registry.yarnpkg.com/@mantine/carousel/-/carousel-6.0.17.tgz#d31fc9bc9ef14bd5ea3e9162d4a130b904cb478e"
integrity sha512-cKX7zGmWVXdq/mPff5QYaHLR2X6bujbR4YZ3Hs3TD8KuySTZDOHipUD9IAVH1DtYJRE0+FIRb6OeZ7X9/N2Erg==
dependencies:
"@mantine/utils" "6.0.17"
"@mantine/core@^6.0.17":
version "6.0.17"
resolved "https://registry.npmjs.org/@mantine/core/-/core-6.0.17.tgz"
@ -1167,6 +1174,13 @@
dependencies:
"@types/react" "*"
"@types/react-grid-layout@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-1.3.2.tgz#9f195666a018a5ae2b773887e3b552cb4378d67f"
integrity sha512-ZzpBEOC1JTQ7MGe1h1cPKSLP4jSWuxc+yvT4TsAlEW9+EFPzAf8nxQfFd7ea9gL17Em7PbwJZAsiwfQQBUklZQ==
dependencies:
"@types/react" "*"
"@types/react-router-dom@^5.3.3":
version "5.3.3"
resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz"
@ -1442,6 +1456,11 @@ clsx@1.1.1:
resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
clsx@^1.1.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz"
@ -1578,6 +1597,24 @@ electron-to-chromium@^1.4.431:
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.468.tgz"
integrity sha512-6M1qyhaJOt7rQtNti1lBA0GwclPH+oKCmsra/hkcWs5INLxfXXD/dtdnaKUYQu/pjOBP/8Osoe4mAcNvvzoFag==
embla-carousel-react@^8.0.0-rc11:
version "8.0.0-rc11"
resolved "https://registry.yarnpkg.com/embla-carousel-react/-/embla-carousel-react-8.0.0-rc11.tgz#0e2fde5cafa3cae9c30721e18aee648599527994"
integrity sha512-hXOAUMOIa0GF5BtdTTqBuKcjgU+ipul6thTCXOZttqnu2c6VS3SIzUUT+onIIEw+AptzKJcPwGcoAByAGa9eJw==
dependencies:
embla-carousel "8.0.0-rc11"
embla-carousel-reactive-utils "8.0.0-rc11"
embla-carousel-reactive-utils@8.0.0-rc11:
version "8.0.0-rc11"
resolved "https://registry.yarnpkg.com/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.0.0-rc11.tgz#d5493bd2bfeb68b1cbf65d4c836a4d36779a03de"
integrity sha512-pDNVJNCn0dybLkHw93My+cMfkRQ5oLZff6ZCwgmrw+96aPiZUyo5ANywz8Lb70SWWgD/TNBRrtQCquvjHS31Sg==
embla-carousel@8.0.0-rc11:
version "8.0.0-rc11"
resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.0.0-rc11.tgz#700ab6b3e4825ef9e6ac83238b81e3e1a316c3f4"
integrity sha512-Toeaug98PGYzSY56p/xsa+u4zbQbAXgGymwEDUc2wqT+1XCnnUsH42MClglhABJQbobwDYxOabhJrfXyJKUMig==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
@ -1987,6 +2024,11 @@ lodash.get@^4.4.2:
resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash.isequal@^4.0.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz"
@ -2231,7 +2273,7 @@ pretty-format@^29.6.1:
ansi-styles "^5.0.0"
react-is "^18.0.0"
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@15.x, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -2270,6 +2312,14 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-draggable@^4.0.0, react-draggable@^4.0.3:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c"
integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==
dependencies:
clsx "^1.1.1"
prop-types "^15.8.1"
react-dropzone@14.2.3:
version "14.2.3"
resolved "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz"
@ -2279,6 +2329,17 @@ react-dropzone@14.2.3:
file-selector "^0.6.0"
prop-types "^15.8.1"
react-grid-layout@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.3.4.tgz#4fa819be24a1ba9268aa11b82d63afc4762a32ff"
integrity sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw==
dependencies:
clsx "^1.1.1"
lodash.isequal "^4.0.0"
prop-types "^15.8.1"
react-draggable "^4.0.0"
react-resizable "^3.0.4"
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
@ -2313,6 +2374,14 @@ react-remove-scroll@^2.5.5:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-resizable@^3.0.4:
version "3.0.5"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.5.tgz#362721f2efbd094976f1780ae13f1ad7739786c1"
integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==
dependencies:
prop-types "15.x"
react-draggable "^4.0.3"
react-router-dom@^6.14.2:
version "6.14.2"
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.2.tgz"