mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-22 01:16:07 +02:00
refactor: replace react-dnd with custom implementation (#988)
* refactor: replace react-dnd with custom implementation * refactor: add TextCell, IconCell, and ActionCell * refactor: port environments list to react-table * refactor: change OfflineBolt to PowerSettingsNew * refactor: simplify environment toast text * refactor: improve IToast type type * refactor: improve useSearchHighlightContext naming * refactor: clarify enableDragAndDrop logic
This commit is contained in:
parent
c073908027
commit
34f848ce8a
@ -83,8 +83,6 @@
|
|||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-chartjs-2": "4.1.0",
|
"react-chartjs-2": "4.1.0",
|
||||||
"react-dnd": "15.1.2",
|
|
||||||
"react-dnd-html5-backend": "15.1.3",
|
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-hooks-global-state": "1.0.2",
|
"react-hooks-global-state": "1.0.2",
|
||||||
"react-router-dom": "6.3.0",
|
"react-router-dom": "6.3.0",
|
||||||
|
@ -4,5 +4,6 @@ const SearchHighlightContext = createContext('');
|
|||||||
|
|
||||||
export const SearchHighlightProvider = SearchHighlightContext.Provider;
|
export const SearchHighlightProvider = SearchHighlightContext.Provider;
|
||||||
|
|
||||||
export const useSearchHighlightContext = () =>
|
export const useSearchHighlightContext = (): { searchQuery: string } => {
|
||||||
useContext(SearchHighlightContext);
|
return { searchQuery: useContext(SearchHighlightContext) };
|
||||||
|
};
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface IContextActionsCellProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ActionCell = ({ children }: IContextActionsCellProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
data-loading
|
||||||
|
sx={{ display: 'flex', justifyContent: 'flex-end', px: 2 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -1,8 +1,8 @@
|
|||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { formatDateYMD, formatDateYMDHMS } from 'utils/formatDate';
|
import { formatDateYMD } from 'utils/formatDate';
|
||||||
import { Box, Tooltip } from '@mui/material';
|
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
|
|
||||||
interface IDateCellProps {
|
interface IDateCellProps {
|
||||||
value?: Date | string | null;
|
value?: Date | string | null;
|
||||||
@ -11,22 +11,11 @@ interface IDateCellProps {
|
|||||||
export const DateCell: VFC<IDateCellProps> = ({ value }) => {
|
export const DateCell: VFC<IDateCellProps> = ({ value }) => {
|
||||||
const { locationSettings } = useLocationSettings();
|
const { locationSettings } = useLocationSettings();
|
||||||
|
|
||||||
if (!value) {
|
const date = value
|
||||||
return <Box sx={{ py: 1.5, px: 2 }} />;
|
? value instanceof Date
|
||||||
}
|
? formatDateYMD(value, locationSettings.locale)
|
||||||
|
: formatDateYMD(parseISO(value), locationSettings.locale)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const date = value instanceof Date ? value : parseISO(value);
|
return <TextCell>{date}</TextCell>;
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ py: 1.5, px: 2 }}>
|
|
||||||
<Tooltip
|
|
||||||
title={formatDateYMDHMS(date, locationSettings.locale)}
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<span data-loading role="tooltip">
|
|
||||||
{formatDateYMD(date, locationSettings.locale)}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface IIconCellProps {
|
||||||
|
icon: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconCell = ({ icon }: IIconCellProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
data-loading
|
||||||
|
sx={{
|
||||||
|
pl: 2,
|
||||||
|
pr: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: 60,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -20,7 +20,7 @@ export const LinkCell: FC<ILinkCellProps> = ({
|
|||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
const search = useSearchHighlightContext();
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@ -32,7 +32,7 @@ export const LinkCell: FC<ILinkCellProps> = ({
|
|||||||
lineClamp: Boolean(subtitle) ? 1 : 2,
|
lineClamp: Boolean(subtitle) ? 1 : 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Highlighter search={search}>{title}</Highlighter>
|
<Highlighter search={searchQuery}>{title}</Highlighter>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -44,7 +44,7 @@ export const LinkCell: FC<ILinkCellProps> = ({
|
|||||||
component="span"
|
component="span"
|
||||||
data-loading
|
data-loading
|
||||||
>
|
>
|
||||||
<Highlighter search={search}>
|
<Highlighter search={searchQuery}>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</Highlighter>
|
</Highlighter>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import { VFC, ReactNode } from 'react';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
|
interface IDateCellProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextCell: VFC<IDateCellProps> = ({ children }) => {
|
||||||
|
if (!children) {
|
||||||
|
return <Box sx={{ py: 1.5, px: 2 }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ py: 1.5, px: 2 }}>
|
||||||
|
<span data-loading role="tooltip">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -1,12 +1,12 @@
|
|||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Delete, Edit } from '@mui/icons-material';
|
import { Delete, Edit } from '@mui/icons-material';
|
||||||
import { Box } from '@mui/material';
|
|
||||||
import {
|
import {
|
||||||
DELETE_CONTEXT_FIELD,
|
DELETE_CONTEXT_FIELD,
|
||||||
UPDATE_CONTEXT_FIELD,
|
UPDATE_CONTEXT_FIELD,
|
||||||
} from 'component/providers/AccessProvider/permissions';
|
} from 'component/providers/AccessProvider/permissions';
|
||||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
|
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||||
|
|
||||||
interface IContextActionsCellProps {
|
interface IContextActionsCellProps {
|
||||||
name: string;
|
name: string;
|
||||||
@ -20,14 +20,7 @@ export const ContextActionsCell: VFC<IContextActionsCellProps> = ({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<ActionCell>
|
||||||
data-loading
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
px: 2,
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
permission={UPDATE_CONTEXT_FIELD}
|
permission={UPDATE_CONTEXT_FIELD}
|
||||||
onClick={() => navigate(`/context/edit/${name}`)}
|
onClick={() => navigate(`/context/edit/${name}`)}
|
||||||
@ -50,6 +43,6 @@ export const ContextActionsCell: VFC<IContextActionsCellProps> = ({
|
|||||||
>
|
>
|
||||||
<Delete />
|
<Delete />
|
||||||
</PermissionIconButton>
|
</PermissionIconButton>
|
||||||
</Box>
|
</ActionCell>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -23,7 +23,7 @@ import { sortTypes } from 'utils/sortTypes';
|
|||||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
import { ContextActionsCell } from './ContextActionsCell/ContextActionsCell';
|
import { ContextActionsCell } from './ContextActionsCell/ContextActionsCell';
|
||||||
import { Adjust } from '@mui/icons-material';
|
import { Adjust } from '@mui/icons-material';
|
||||||
import { Box } from '@mui/material';
|
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||||
|
|
||||||
const ContextList: VFC = () => {
|
const ContextList: VFC = () => {
|
||||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||||
@ -53,19 +53,7 @@ const ContextList: VFC = () => {
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: 'Icon',
|
id: 'Icon',
|
||||||
Cell: () => (
|
Cell: () => <IconCell icon={<Adjust color="disabled" />} />,
|
||||||
<Box
|
|
||||||
data-loading
|
|
||||||
sx={{
|
|
||||||
pl: 2,
|
|
||||||
pr: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Adjust color="disabled" />
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Name',
|
Header: 'Name',
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||||
|
import { Add } from '@mui/icons-material';
|
||||||
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const CreateEnvironmentButton = () => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveButton
|
||||||
|
onClick={() => navigate('/environments/create')}
|
||||||
|
maxWidth="700px"
|
||||||
|
Icon={Add}
|
||||||
|
permission={ADMIN}
|
||||||
|
disabled={!Boolean(uiConfig.flags.EEA)}
|
||||||
|
>
|
||||||
|
New Environment
|
||||||
|
</ResponsiveButton>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,193 @@
|
|||||||
|
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||||
|
import {
|
||||||
|
DELETE_ENVIRONMENT,
|
||||||
|
UPDATE_ENVIRONMENT,
|
||||||
|
} from 'component/providers/AccessProvider/permissions';
|
||||||
|
import {
|
||||||
|
Edit,
|
||||||
|
Delete,
|
||||||
|
OfflineBolt,
|
||||||
|
DragIndicator,
|
||||||
|
PowerSettingsNew,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { IconButton, Tooltip } from '@mui/material';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import AccessContext from 'contexts/AccessContext';
|
||||||
|
import { useContext, useState } from 'react';
|
||||||
|
import { IEnvironment } from 'interfaces/environments';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import EnvironmentToggleConfirm from '../EnvironmentToggleConfirm/EnvironmentToggleConfirm';
|
||||||
|
import EnvironmentDeleteConfirm from '../EnvironmentDeleteConfirm/EnvironmentDeleteConfirm';
|
||||||
|
import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||||
|
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||||
|
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { useId } from 'hooks/useId';
|
||||||
|
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
|
||||||
|
interface IEnvironmentTableActionsProps {
|
||||||
|
environment: IEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EnvironmentActionCell = ({
|
||||||
|
environment,
|
||||||
|
}: IEnvironmentTableActionsProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const updatePermission = hasAccess(UPDATE_ENVIRONMENT);
|
||||||
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
|
||||||
|
const { setToastApiError, setToastData } = useToast();
|
||||||
|
const { refetchEnvironments } = useEnvironments();
|
||||||
|
const { refetch: refetchPermissions } = useProjectRolePermissions();
|
||||||
|
const { deleteEnvironment, toggleEnvironmentOn, toggleEnvironmentOff } =
|
||||||
|
useEnvironmentApi();
|
||||||
|
|
||||||
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
|
const [toggleModal, setToggleModal] = useState(false);
|
||||||
|
const [confirmName, setConfirmName] = useState('');
|
||||||
|
|
||||||
|
const handleDeleteEnvironment = async () => {
|
||||||
|
try {
|
||||||
|
await deleteEnvironment(environment.name);
|
||||||
|
refetchPermissions();
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Project environment deleted',
|
||||||
|
text: 'You have successfully deleted the project environment.',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
} finally {
|
||||||
|
setDeleteModal(false);
|
||||||
|
setConfirmName('');
|
||||||
|
await refetchEnvironments();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmToggleEnvironment = () => {
|
||||||
|
return environment.enabled
|
||||||
|
? handleToggleEnvironmentOff()
|
||||||
|
: handleToggleEnvironmentOn();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleEnvironmentOn = async () => {
|
||||||
|
try {
|
||||||
|
await toggleEnvironmentOn(environment.name);
|
||||||
|
setToggleModal(false);
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Project environment enabled',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
} finally {
|
||||||
|
await refetchEnvironments();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleEnvironmentOff = async () => {
|
||||||
|
try {
|
||||||
|
await toggleEnvironmentOff(environment.name);
|
||||||
|
setToggleModal(false);
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Project environment disabled',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
} finally {
|
||||||
|
await refetchEnvironments();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleIconTooltip = environment.enabled
|
||||||
|
? 'Disable environment'
|
||||||
|
: 'Enable environment';
|
||||||
|
|
||||||
|
const editId = useId();
|
||||||
|
const deleteId = useId();
|
||||||
|
|
||||||
|
// Allow drag and drop if the user is permitted to reorder environments.
|
||||||
|
// Disable drag and drop while searching since some rows may be hidden.
|
||||||
|
const enableDragAndDrop = updatePermission && !searchQuery;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionCell>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={enableDragAndDrop}
|
||||||
|
show={
|
||||||
|
<IconButton size="large">
|
||||||
|
<DragIndicator titleAccess="Drag" cursor="grab" />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={updatePermission}
|
||||||
|
show={
|
||||||
|
<Tooltip title={toggleIconTooltip} arrow>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setToggleModal(true)}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<PowerSettingsNew />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={updatePermission}
|
||||||
|
show={
|
||||||
|
<Tooltip title="Edit environment" arrow>
|
||||||
|
<span id={editId}>
|
||||||
|
<IconButton
|
||||||
|
aria-describedby={editId}
|
||||||
|
disabled={environment.protected}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(
|
||||||
|
`/environments/${environment.name}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(DELETE_ENVIRONMENT)}
|
||||||
|
show={
|
||||||
|
<Tooltip title="Delete environment" arrow>
|
||||||
|
<span id={deleteId}>
|
||||||
|
<IconButton
|
||||||
|
aria-describedby={deleteId}
|
||||||
|
disabled={environment.protected}
|
||||||
|
onClick={() => setDeleteModal(true)}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<EnvironmentDeleteConfirm
|
||||||
|
env={environment}
|
||||||
|
setDeldialogue={setDeleteModal}
|
||||||
|
open={deleteModal}
|
||||||
|
handleDeleteEnvironment={handleDeleteEnvironment}
|
||||||
|
confirmName={confirmName}
|
||||||
|
setConfirmName={setConfirmName}
|
||||||
|
/>
|
||||||
|
<EnvironmentToggleConfirm
|
||||||
|
env={environment}
|
||||||
|
open={toggleModal}
|
||||||
|
setToggleDialog={setToggleModal}
|
||||||
|
handleConfirmToggleEnvironment={handleConfirmToggleEnvironment}
|
||||||
|
/>
|
||||||
|
</ActionCell>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { CloudCircle } from '@mui/icons-material';
|
import { CloudCircle } from '@mui/icons-material';
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||||
import { useStyles } from './EnvironmentCard.styles';
|
import { useStyles } from 'component/environments/EnvironmentCard/EnvironmentCard.styles';
|
||||||
|
|
||||||
interface IEnvironmentProps {
|
interface IEnvironmentProps {
|
||||||
name: string;
|
name: string;
|
@ -3,13 +3,12 @@ import React from 'react';
|
|||||||
import { IEnvironment } from 'interfaces/environments';
|
import { IEnvironment } from 'interfaces/environments';
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import Input from 'component/common/Input/Input';
|
import Input from 'component/common/Input/Input';
|
||||||
import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
|
import EnvironmentCard from 'component/environments/EnvironmentCard/EnvironmentCard';
|
||||||
import { useStyles } from './EnvironmentDeleteConfirm.styles';
|
import { useStyles } from 'component/environments/EnvironmentDeleteConfirm/EnvironmentDeleteConfirm.styles';
|
||||||
|
|
||||||
interface IEnviromentDeleteConfirmProps {
|
interface IEnviromentDeleteConfirmProps {
|
||||||
env: IEnvironment;
|
env: IEnvironment;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>;
|
|
||||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
handleDeleteEnvironment: () => Promise<void>;
|
handleDeleteEnvironment: () => Promise<void>;
|
||||||
confirmName: string;
|
confirmName: string;
|
@ -1,202 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { List } from '@mui/material';
|
|
||||||
import { Add } from '@mui/icons-material';
|
|
||||||
import useToast from 'hooks/useToast';
|
|
||||||
import { IEnvironment, ISortOrderPayload } from 'interfaces/environments';
|
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
||||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
|
||||||
import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
|
||||||
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
|
||||||
import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem';
|
|
||||||
import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm';
|
|
||||||
import EnvironmentDeleteConfirm from './EnvironmentDeleteConfirm/EnvironmentDeleteConfirm';
|
|
||||||
|
|
||||||
const EnvironmentList = () => {
|
|
||||||
const defaultEnv = {
|
|
||||||
name: '',
|
|
||||||
type: '',
|
|
||||||
sortOrder: 0,
|
|
||||||
createdAt: '',
|
|
||||||
enabled: true,
|
|
||||||
protected: false,
|
|
||||||
};
|
|
||||||
const { environments, mutateEnvironments, refetchEnvironments } =
|
|
||||||
useEnvironments();
|
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
const { refetch: refetchProjectRolePermissions } =
|
|
||||||
useProjectRolePermissions();
|
|
||||||
|
|
||||||
const [selectedEnv, setSelectedEnv] = useState(defaultEnv);
|
|
||||||
const [delDialog, setDeldialogue] = useState(false);
|
|
||||||
const [toggleDialog, setToggleDialog] = useState(false);
|
|
||||||
const [confirmName, setConfirmName] = useState('');
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { setToastApiError, setToastData } = useToast();
|
|
||||||
const {
|
|
||||||
deleteEnvironment,
|
|
||||||
changeSortOrder,
|
|
||||||
toggleEnvironmentOn,
|
|
||||||
toggleEnvironmentOff,
|
|
||||||
} = useEnvironmentApi();
|
|
||||||
|
|
||||||
const moveListItem = (dragIndex: number, hoverIndex: number) => {
|
|
||||||
const newEnvList = [...environments];
|
|
||||||
if (newEnvList.length === 0) return newEnvList;
|
|
||||||
|
|
||||||
const item = newEnvList.splice(dragIndex, 1)[0];
|
|
||||||
|
|
||||||
newEnvList.splice(hoverIndex, 0, item);
|
|
||||||
mutateEnvironments(newEnvList);
|
|
||||||
return newEnvList;
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveListItemApi = async (dragIndex: number, hoverIndex: number) => {
|
|
||||||
const newEnvList = moveListItem(dragIndex, hoverIndex);
|
|
||||||
const sortOrder = newEnvList.reduce(
|
|
||||||
(acc: ISortOrderPayload, env: IEnvironment, index: number) => {
|
|
||||||
acc[env.name] = index + 1;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await sortOrderAPICall(sortOrder);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortOrderAPICall = async (sortOrder: ISortOrderPayload) => {
|
|
||||||
try {
|
|
||||||
await changeSortOrder(sortOrder);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteEnvironment = async () => {
|
|
||||||
try {
|
|
||||||
await deleteEnvironment(selectedEnv.name);
|
|
||||||
refetchProjectRolePermissions();
|
|
||||||
setToastData({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Project environment deleted',
|
|
||||||
text: 'You have successfully deleted the project environment.',
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
} finally {
|
|
||||||
setDeldialogue(false);
|
|
||||||
setSelectedEnv(defaultEnv);
|
|
||||||
setConfirmName('');
|
|
||||||
refetchEnvironments();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmToggleEnvironment = () => {
|
|
||||||
if (selectedEnv.enabled) {
|
|
||||||
return handleToggleEnvironmentOff();
|
|
||||||
}
|
|
||||||
handleToggleEnvironmentOn();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleEnvironmentOn = async () => {
|
|
||||||
try {
|
|
||||||
await toggleEnvironmentOn(selectedEnv.name);
|
|
||||||
setToggleDialog(false);
|
|
||||||
|
|
||||||
setToastData({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Project environment enabled',
|
|
||||||
text: 'Your environment is enabled',
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
} finally {
|
|
||||||
refetchEnvironments();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleEnvironmentOff = async () => {
|
|
||||||
try {
|
|
||||||
await toggleEnvironmentOff(selectedEnv.name);
|
|
||||||
setToggleDialog(false);
|
|
||||||
setToastData({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Project environment disabled',
|
|
||||||
text: 'Your environment is disabled.',
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
} finally {
|
|
||||||
refetchEnvironments();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const environmentList = () =>
|
|
||||||
environments.map((env: IEnvironment, index: number) => (
|
|
||||||
<EnvironmentListItem
|
|
||||||
key={env.name}
|
|
||||||
env={env}
|
|
||||||
setDeldialogue={setDeldialogue}
|
|
||||||
setSelectedEnv={setSelectedEnv}
|
|
||||||
setToggleDialog={setToggleDialog}
|
|
||||||
index={index}
|
|
||||||
moveListItem={moveListItem}
|
|
||||||
moveListItemApi={moveListItemApi}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
const navigateToCreateEnvironment = () => {
|
|
||||||
navigate('/environments/create');
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<PageContent
|
|
||||||
header={
|
|
||||||
<PageHeader
|
|
||||||
title="Environments"
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<ResponsiveButton
|
|
||||||
onClick={navigateToCreateEnvironment}
|
|
||||||
maxWidth="700px"
|
|
||||||
Icon={Add}
|
|
||||||
permission={ADMIN}
|
|
||||||
disabled={!Boolean(uiConfig.flags.EEA)}
|
|
||||||
>
|
|
||||||
New Environment
|
|
||||||
</ResponsiveButton>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<List>{environmentList()}</List>
|
|
||||||
<EnvironmentDeleteConfirm
|
|
||||||
env={selectedEnv}
|
|
||||||
setSelectedEnv={setSelectedEnv}
|
|
||||||
setDeldialogue={setDeldialogue}
|
|
||||||
open={delDialog}
|
|
||||||
handleDeleteEnvironment={handleDeleteEnvironment}
|
|
||||||
confirmName={confirmName}
|
|
||||||
setConfirmName={setConfirmName}
|
|
||||||
/>
|
|
||||||
<EnvironmentToggleConfirm
|
|
||||||
env={selectedEnv}
|
|
||||||
open={toggleDialog}
|
|
||||||
setToggleDialog={setToggleDialog}
|
|
||||||
handleConfirmToggleEnvironment={handleConfirmToggleEnvironment}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnvironmentList;
|
|
@ -1,219 +0,0 @@
|
|||||||
import {
|
|
||||||
ListItem,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
Tooltip,
|
|
||||||
IconButton,
|
|
||||||
} from '@mui/material';
|
|
||||||
import {
|
|
||||||
CloudCircle,
|
|
||||||
Delete,
|
|
||||||
DragIndicator,
|
|
||||||
Edit,
|
|
||||||
OfflineBolt,
|
|
||||||
} from '@mui/icons-material';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
|
|
||||||
import { IEnvironment } from 'interfaces/environments';
|
|
||||||
import React, { useContext, useRef } from 'react';
|
|
||||||
import AccessContext from 'contexts/AccessContext';
|
|
||||||
import {
|
|
||||||
DELETE_ENVIRONMENT,
|
|
||||||
UPDATE_ENVIRONMENT,
|
|
||||||
} from 'component/providers/AccessProvider/permissions';
|
|
||||||
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd';
|
|
||||||
import { XYCoord, Identifier } from 'dnd-core';
|
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { StatusBadge } from 'component/common/StatusBadge/StatusBadge';
|
|
||||||
|
|
||||||
interface IEnvironmentListItemProps {
|
|
||||||
env: IEnvironment;
|
|
||||||
setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>;
|
|
||||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
setToggleDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
index: number;
|
|
||||||
moveListItem: (dragIndex: number, hoverIndex: number) => IEnvironment[];
|
|
||||||
moveListItemApi: (dragIndex: number, hoverIndex: number) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IDragItem {
|
|
||||||
index: number;
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ICollectedProps {
|
|
||||||
handlerId: Identifier | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EnvironmentListItem = ({
|
|
||||||
env,
|
|
||||||
setSelectedEnv,
|
|
||||||
setDeldialogue,
|
|
||||||
index,
|
|
||||||
moveListItem,
|
|
||||||
moveListItemApi,
|
|
||||||
setToggleDialog,
|
|
||||||
}: IEnvironmentListItemProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const ref = useRef<HTMLLIElement>(null);
|
|
||||||
const ACCEPT_TYPE = 'LIST_ITEM';
|
|
||||||
const [{ isDragging }, drag] = useDrag({
|
|
||||||
type: ACCEPT_TYPE,
|
|
||||||
item: () => {
|
|
||||||
return { env, index };
|
|
||||||
},
|
|
||||||
collect: (monitor: any) => ({
|
|
||||||
isDragging: monitor.isDragging(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const [{ handlerId }, drop] = useDrop<IDragItem, unknown, ICollectedProps>({
|
|
||||||
accept: ACCEPT_TYPE,
|
|
||||||
collect(monitor) {
|
|
||||||
return {
|
|
||||||
handlerId: monitor.getHandlerId(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
drop(item: IDragItem, monitor: DropTargetMonitor) {
|
|
||||||
const dragIndex = item.index;
|
|
||||||
const hoverIndex = index;
|
|
||||||
moveListItemApi(dragIndex, hoverIndex);
|
|
||||||
},
|
|
||||||
hover(item: IDragItem, monitor: DropTargetMonitor) {
|
|
||||||
if (!ref.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dragIndex = item.index;
|
|
||||||
const hoverIndex = index;
|
|
||||||
|
|
||||||
if (dragIndex === hoverIndex) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
|
||||||
|
|
||||||
const hoverMiddleY =
|
|
||||||
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
|
||||||
|
|
||||||
const clientOffset = monitor.getClientOffset();
|
|
||||||
|
|
||||||
const hoverClientY =
|
|
||||||
(clientOffset as XYCoord).y - hoverBoundingRect.top;
|
|
||||||
|
|
||||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
moveListItem(dragIndex, hoverIndex);
|
|
||||||
item.index = hoverIndex;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const opacity = isDragging ? 0 : 1;
|
|
||||||
|
|
||||||
const { hasAccess } = useContext(AccessContext);
|
|
||||||
const updatePermission = hasAccess(UPDATE_ENVIRONMENT);
|
|
||||||
const tooltipText = env.enabled ? 'Disable' : 'Enable';
|
|
||||||
|
|
||||||
if (updatePermission) {
|
|
||||||
drag(drop(ref));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
style={{ position: 'relative', opacity }}
|
|
||||||
ref={ref}
|
|
||||||
data-handler-id={handlerId}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
|
||||||
<CloudCircle />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
<>
|
|
||||||
<strong>
|
|
||||||
<StringTruncator
|
|
||||||
text={env.name}
|
|
||||||
maxWidth={'125'}
|
|
||||||
maxLength={25}
|
|
||||||
/>
|
|
||||||
</strong>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={!env.enabled}
|
|
||||||
show={
|
|
||||||
<StatusBadge severity="warning">
|
|
||||||
Disabled
|
|
||||||
</StatusBadge>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={updatePermission}
|
|
||||||
show={
|
|
||||||
<IconButton size="large">
|
|
||||||
<DragIndicator titleAccess="Drag" cursor="grab" />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={updatePermission}
|
|
||||||
show={
|
|
||||||
<Tooltip title={`${tooltipText} environment`} arrow>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedEnv(env);
|
|
||||||
setToggleDialog(prev => !prev);
|
|
||||||
}}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<OfflineBolt />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={updatePermission}
|
|
||||||
show={
|
|
||||||
<Tooltip title="Edit environment" arrow>
|
|
||||||
<IconButton
|
|
||||||
disabled={env.protected}
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/environments/${env.name}`);
|
|
||||||
}}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<Edit />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasAccess(DELETE_ENVIRONMENT)}
|
|
||||||
show={
|
|
||||||
<Tooltip title="Delete environment" arrow>
|
|
||||||
<IconButton
|
|
||||||
disabled={env.protected}
|
|
||||||
onClick={() => {
|
|
||||||
setDeldialogue(true);
|
|
||||||
setSelectedEnv(env);
|
|
||||||
}}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<Delete />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnvironmentListItem;
|
|
@ -0,0 +1,22 @@
|
|||||||
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
|
import { IEnvironment } from 'interfaces/environments';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { StatusBadge } from 'component/common/StatusBadge/StatusBadge';
|
||||||
|
|
||||||
|
interface IEnvironmentNameCellProps {
|
||||||
|
environment: IEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EnvironmentNameCell = ({
|
||||||
|
environment,
|
||||||
|
}: IEnvironmentNameCellProps) => {
|
||||||
|
return (
|
||||||
|
<TextCell>
|
||||||
|
{environment.name}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!environment.enabled}
|
||||||
|
show={<StatusBadge severity="warning">Disabled</StatusBadge>}
|
||||||
|
/>
|
||||||
|
</TextCell>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
import { useDragItem, MoveListItem } from 'hooks/useDragItem';
|
||||||
|
import { Row } from 'react-table';
|
||||||
|
import { TableRow } from '@mui/material';
|
||||||
|
import { TableCell } from 'component/common/Table';
|
||||||
|
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
|
||||||
|
interface IEnvironmentRowProps {
|
||||||
|
row: Row;
|
||||||
|
moveListItem: MoveListItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EnvironmentRow = ({ row, moveListItem }: IEnvironmentRowProps) => {
|
||||||
|
const dragItemRef = useDragItem(row.index, moveListItem);
|
||||||
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
const draggable = !searchQuery;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow hover ref={draggable ? dragItemRef : undefined}>
|
||||||
|
{row.cells.map((cell: any) => (
|
||||||
|
<TableCell {...cell.getCellProps()}>
|
||||||
|
{cell.render('Cell')}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,128 @@
|
|||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||||
|
import { CreateEnvironmentButton } from 'component/environments/CreateEnvironmentButton/CreateEnvironmentButton';
|
||||||
|
import { useTable, useGlobalFilter } from 'react-table';
|
||||||
|
import {
|
||||||
|
TableSearch,
|
||||||
|
SortableTableHeader,
|
||||||
|
Table,
|
||||||
|
} from 'component/common/Table';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { TableBody } from '@mui/material';
|
||||||
|
import { CloudCircle } from '@mui/icons-material';
|
||||||
|
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||||
|
import { EnvironmentActionCell } from 'component/environments/EnvironmentActionCell/EnvironmentActionCell';
|
||||||
|
import { EnvironmentNameCell } from 'component/environments/EnvironmentNameCell/EnvironmentNameCell';
|
||||||
|
import { EnvironmentRow } from 'component/environments/EnvironmentRow/EnvironmentRow';
|
||||||
|
import { MoveListItem } from 'hooks/useDragItem';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import useEnvironmentApi, {
|
||||||
|
createSortOrderPayload,
|
||||||
|
} from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
|
||||||
|
export const EnvironmentTable = () => {
|
||||||
|
const { changeSortOrder } = useEnvironmentApi();
|
||||||
|
const { setToastApiError } = useToast();
|
||||||
|
const { environments, mutateEnvironments } = useEnvironments();
|
||||||
|
|
||||||
|
const moveListItem: MoveListItem = useCallback(
|
||||||
|
async (dragIndex: number, dropIndex: number, save = false) => {
|
||||||
|
const copy = [...environments];
|
||||||
|
const tmp = copy[dragIndex];
|
||||||
|
copy.splice(dragIndex, 1);
|
||||||
|
copy.splice(dropIndex, 0, tmp);
|
||||||
|
await mutateEnvironments(copy);
|
||||||
|
|
||||||
|
if (save) {
|
||||||
|
try {
|
||||||
|
await changeSortOrder(createSortOrderPayload(copy));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[changeSortOrder, environments, mutateEnvironments, setToastApiError]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
prepareRow,
|
||||||
|
state: { globalFilter },
|
||||||
|
setGlobalFilter,
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns: COLUMNS as any,
|
||||||
|
data: environments,
|
||||||
|
autoResetGlobalFilter: false,
|
||||||
|
},
|
||||||
|
useGlobalFilter
|
||||||
|
);
|
||||||
|
|
||||||
|
const headerSearch = (
|
||||||
|
<TableSearch initialValue={globalFilter} onChange={setGlobalFilter} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const headerActions = (
|
||||||
|
<>
|
||||||
|
{headerSearch}
|
||||||
|
<PageHeader.Divider />
|
||||||
|
<CreateEnvironmentButton />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const header = <PageHeader title="Environments" actions={headerActions} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent header={header}>
|
||||||
|
<SearchHighlightProvider value={globalFilter}>
|
||||||
|
<Table {...getTableProps()}>
|
||||||
|
<SortableTableHeader headerGroups={headerGroups as any} />
|
||||||
|
<TableBody {...getTableBodyProps()}>
|
||||||
|
{rows.map(row => {
|
||||||
|
prepareRow(row);
|
||||||
|
return (
|
||||||
|
<EnvironmentRow
|
||||||
|
row={row as any}
|
||||||
|
moveListItem={moveListItem}
|
||||||
|
key={row.original.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</SearchHighlightProvider>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLUMNS = [
|
||||||
|
{
|
||||||
|
id: 'Icon',
|
||||||
|
canSort: false,
|
||||||
|
Cell: () => <IconCell icon={<CloudCircle color="disabled" />} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'name',
|
||||||
|
width: '100%',
|
||||||
|
canSort: false,
|
||||||
|
Cell: (props: any) => (
|
||||||
|
<EnvironmentNameCell environment={props.row.original} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Actions',
|
||||||
|
id: 'Actions',
|
||||||
|
align: 'center',
|
||||||
|
canSort: false,
|
||||||
|
Cell: (props: any) => (
|
||||||
|
<EnvironmentActionCell environment={props.row.original} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||||||
import { IEnvironment } from 'interfaces/environments';
|
import { IEnvironment } from 'interfaces/environments';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
|
import EnvironmentCard from 'component/environments/EnvironmentCard/EnvironmentCard';
|
||||||
|
|
||||||
interface IEnvironmentToggleConfirmProps {
|
interface IEnvironmentToggleConfirmProps {
|
||||||
env: IEnvironment;
|
env: IEnvironment;
|
@ -18,7 +18,6 @@ import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPasswor
|
|||||||
import { ProjectListNew } from 'component/project/ProjectList/ProjectList';
|
import { ProjectListNew } from 'component/project/ProjectList/ProjectList';
|
||||||
import Project from 'component/project/Project/Project';
|
import Project from 'component/project/Project/Project';
|
||||||
import RedirectArchive from 'component/archive/RedirectArchive';
|
import RedirectArchive from 'component/archive/RedirectArchive';
|
||||||
import EnvironmentList from 'component/environments/EnvironmentList/EnvironmentList';
|
|
||||||
import { FeatureView } from 'component/feature/FeatureView/FeatureView';
|
import { FeatureView } from 'component/feature/FeatureView/FeatureView';
|
||||||
import ProjectRoles from 'component/admin/projectRoles/ProjectRoles/ProjectRoles';
|
import ProjectRoles from 'component/admin/projectRoles/ProjectRoles/ProjectRoles';
|
||||||
import CreateProjectRole from 'component/admin/projectRoles/CreateProjectRole/CreateProjectRole';
|
import CreateProjectRole from 'component/admin/projectRoles/CreateProjectRole/CreateProjectRole';
|
||||||
@ -52,6 +51,7 @@ import { CreateSegment } from 'component/segments/CreateSegment/CreateSegment';
|
|||||||
import { EditSegment } from 'component/segments/EditSegment/EditSegment';
|
import { EditSegment } from 'component/segments/EditSegment/EditSegment';
|
||||||
import { SegmentsList } from 'component/segments/SegmentList/SegmentList';
|
import { SegmentsList } from 'component/segments/SegmentList/SegmentList';
|
||||||
import { IRoute } from 'interfaces/route';
|
import { IRoute } from 'interfaces/route';
|
||||||
|
import { EnvironmentTable } from 'component/environments/EnvironmentTable/EnvironmentTable';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -266,7 +266,7 @@ export const routes: IRoute[] = [
|
|||||||
{
|
{
|
||||||
path: '/environments',
|
path: '/environments',
|
||||||
title: 'Environments',
|
title: 'Environments',
|
||||||
component: EnvironmentList,
|
component: EnvironmentTable,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
flag: EEA,
|
flag: EEA,
|
||||||
menu: { mobile: true, advanced: true },
|
menu: { mobile: true, advanced: true },
|
||||||
|
@ -10,12 +10,12 @@ interface IUIContext {
|
|||||||
|
|
||||||
export const createEmptyToast = (): IToast => {
|
export const createEmptyToast = (): IToast => {
|
||||||
return {
|
return {
|
||||||
|
type: 'success',
|
||||||
title: '',
|
title: '',
|
||||||
text: '',
|
text: '',
|
||||||
components: [],
|
components: [],
|
||||||
show: false,
|
show: false,
|
||||||
persist: false,
|
persist: false,
|
||||||
type: '',
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
IEnvironmentPayload,
|
IEnvironmentPayload,
|
||||||
ISortOrderPayload,
|
ISortOrderPayload,
|
||||||
IEnvironmentEditPayload,
|
IEnvironmentEditPayload,
|
||||||
|
IEnvironment,
|
||||||
} from 'interfaces/environments';
|
} from 'interfaces/environments';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
@ -145,4 +146,13 @@ const useEnvironmentApi = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createSortOrderPayload = (
|
||||||
|
environments: Readonly<IEnvironment[]>
|
||||||
|
): ISortOrderPayload => {
|
||||||
|
return environments.reduce((payload, env, index) => {
|
||||||
|
payload[env.name] = index + 1;
|
||||||
|
return payload;
|
||||||
|
}, {} as ISortOrderPayload);
|
||||||
|
};
|
||||||
|
|
||||||
export default useEnvironmentApi;
|
export default useEnvironmentApi;
|
||||||
|
@ -13,13 +13,13 @@ interface IUseEnvironmentsOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useEnvironments = (): IUseEnvironmentsOutput => {
|
export const useEnvironments = (): IUseEnvironmentsOutput => {
|
||||||
const { data, error, mutate } = useSWR<IEnvironmentResponse>(
|
const { data, error, mutate } = useSWR<IEnvironment[]>(
|
||||||
formatApiPath(`api/admin/environments`),
|
formatApiPath(`api/admin/environments`),
|
||||||
fetcher
|
fetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const environments = useMemo(() => {
|
const environments = useMemo(() => {
|
||||||
return data?.environments || [];
|
return data || [];
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const refetchEnvironments = useCallback(async () => {
|
const refetchEnvironments = useCallback(async () => {
|
||||||
@ -28,7 +28,7 @@ export const useEnvironments = (): IUseEnvironmentsOutput => {
|
|||||||
|
|
||||||
const mutateEnvironments = useCallback(
|
const mutateEnvironments = useCallback(
|
||||||
async (environments: IEnvironment[]) => {
|
async (environments: IEnvironment[]) => {
|
||||||
await mutate({ environments }, false);
|
await mutate(environments, false);
|
||||||
},
|
},
|
||||||
[mutate]
|
[mutate]
|
||||||
);
|
);
|
||||||
@ -42,8 +42,12 @@ export const useEnvironments = (): IUseEnvironmentsOutput => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetcher = (path: string): Promise<IEnvironmentResponse> => {
|
const fetcher = async (path: string): Promise<IEnvironment[]> => {
|
||||||
return fetch(path)
|
const res: IEnvironmentResponse = await fetch(path)
|
||||||
.then(handleErrorResponses('Environments'))
|
.then(handleErrorResponses('Environments'))
|
||||||
.then(res => res.json());
|
.then(res => res.json());
|
||||||
|
|
||||||
|
return res.environments.sort((a, b) => {
|
||||||
|
return a.sortOrder - b.sortOrder;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
72
frontend/src/hooks/useDragItem.ts
Normal file
72
frontend/src/hooks/useDragItem.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { useRef, useEffect, RefObject } from 'react';
|
||||||
|
|
||||||
|
export type MoveListItem = (
|
||||||
|
dragIndex: number,
|
||||||
|
dropIndex: number,
|
||||||
|
save?: boolean
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export const useDragItem = (
|
||||||
|
listItemIndex: number,
|
||||||
|
moveListItem: MoveListItem
|
||||||
|
): RefObject<HTMLTableRowElement> => {
|
||||||
|
const ref = useRef<HTMLTableRowElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.draggable = true;
|
||||||
|
ref.current.style.cursor = 'grab';
|
||||||
|
ref.current.dataset.index = String(listItemIndex);
|
||||||
|
return addEventListeners(ref.current, moveListItem);
|
||||||
|
}
|
||||||
|
}, [listItemIndex, moveListItem]);
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addEventListeners = (
|
||||||
|
el: HTMLTableRowElement,
|
||||||
|
moveListItem: MoveListItem
|
||||||
|
): (() => void) => {
|
||||||
|
const moveDraggedElement = (save: boolean) => {
|
||||||
|
if (globalDraggedElement) {
|
||||||
|
moveListItem(
|
||||||
|
Number(globalDraggedElement.dataset.index),
|
||||||
|
Number(el.dataset.index),
|
||||||
|
save
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragStart = () => {
|
||||||
|
globalDraggedElement = el;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnter = () => {
|
||||||
|
moveDraggedElement(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragOver = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrop = () => {
|
||||||
|
moveDraggedElement(true);
|
||||||
|
globalDraggedElement = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener('dragstart', onDragStart);
|
||||||
|
el.addEventListener('dragenter', onDragEnter);
|
||||||
|
el.addEventListener('dragover', onDragOver);
|
||||||
|
el.addEventListener('drop', onDrop);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener('dragstart', onDragStart);
|
||||||
|
el.removeEventListener('dragenter', onDragEnter);
|
||||||
|
el.removeEventListener('dragover', onDragOver);
|
||||||
|
el.removeEventListener('drop', onDrop);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// The element being dragged in the browser.
|
||||||
|
let globalDraggedElement: HTMLTableRowElement | null;
|
@ -4,8 +4,6 @@ import 'regenerator-runtime/runtime';
|
|||||||
|
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { DndProvider } from 'react-dnd';
|
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
||||||
import { ThemeProvider } from 'themes/ThemeProvider';
|
import { ThemeProvider } from 'themes/ThemeProvider';
|
||||||
import { App } from 'component/App';
|
import { App } from 'component/App';
|
||||||
import { ScrollTop } from 'component/common/ScrollTop/ScrollTop';
|
import { ScrollTop } from 'component/common/ScrollTop/ScrollTop';
|
||||||
@ -17,23 +15,21 @@ import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/
|
|||||||
import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
|
import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<DndProvider backend={HTML5Backend}>
|
<UIProvider>
|
||||||
<UIProvider>
|
<AccessProvider>
|
||||||
<AccessProvider>
|
<BrowserRouter basename={basePath}>
|
||||||
<BrowserRouter basename={basePath}>
|
<ThemeProvider>
|
||||||
<ThemeProvider>
|
<AnnouncerProvider>
|
||||||
<AnnouncerProvider>
|
<FeedbackCESProvider>
|
||||||
<FeedbackCESProvider>
|
<InstanceStatus>
|
||||||
<InstanceStatus>
|
<ScrollTop />
|
||||||
<ScrollTop />
|
<App />
|
||||||
<App />
|
</InstanceStatus>
|
||||||
</InstanceStatus>
|
</FeedbackCESProvider>
|
||||||
</FeedbackCESProvider>
|
</AnnouncerProvider>
|
||||||
</AnnouncerProvider>
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
</BrowserRouter>
|
||||||
</BrowserRouter>
|
</AccessProvider>
|
||||||
</AccessProvider>
|
</UIProvider>,
|
||||||
</UIProvider>
|
|
||||||
</DndProvider>,
|
|
||||||
document.getElementById('app')
|
document.getElementById('app')
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export interface IToast {
|
export interface IToast {
|
||||||
type: string;
|
type: 'success' | 'error';
|
||||||
title: string;
|
title: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
components?: JSX.Element[];
|
components?: JSX.Element[];
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"globalDependencies": {
|
|
||||||
"react-dnd": "registry:dt/react-dnd#2.0.2+20161111212335",
|
|
||||||
}
|
|
||||||
}
|
|
@ -1538,21 +1538,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
|
||||||
integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==
|
integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==
|
||||||
|
|
||||||
"@react-dnd/asap@4.0.1":
|
|
||||||
version "4.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
|
|
||||||
integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==
|
|
||||||
|
|
||||||
"@react-dnd/invariant@3.0.1":
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-3.0.1.tgz#7e70be19ea21b539e8bf1da28466f4f05df2a4cc"
|
|
||||||
integrity sha512-blqduwV86oiKw2Gr44wbe3pj3Z/OsXirc7ybCv9F/pLAR+Aih8F3rjeJzK0ANgtYKv5lCpkGVoZAeKitKDaD/g==
|
|
||||||
|
|
||||||
"@react-dnd/shallowequal@3.0.1":
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-3.0.1.tgz#8056fe046a8d10a275e321ec0557ae652d7a4d06"
|
|
||||||
integrity sha512-XjDVbs3ZU16CO1h5Q3Ew2RPJqmZBDE/EVf1LYp6ePEffs3V/MX9ZbL5bJr8qiK5SbGmUMuDoaFgyKacYz8prRA==
|
|
||||||
|
|
||||||
"@rollup/pluginutils@^4.2.1":
|
"@rollup/pluginutils@^4.2.1":
|
||||||
version "4.2.1"
|
version "4.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
|
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
|
||||||
@ -3040,15 +3025,6 @@ dir-glob@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
path-type "^4.0.0"
|
path-type "^4.0.0"
|
||||||
|
|
||||||
dnd-core@15.1.2:
|
|
||||||
version "15.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-15.1.2.tgz#0983bce555c4985f58b731ffe1faed31e1ea7f6f"
|
|
||||||
integrity sha512-EOec1LyJUuGRFg0LDa55rSRAUe97uNVKVkUo8iyvzQlcECYTuPblVQfRWXWj1OyPseFIeebWpNmKFy0h6BcF1A==
|
|
||||||
dependencies:
|
|
||||||
"@react-dnd/asap" "4.0.1"
|
|
||||||
"@react-dnd/invariant" "3.0.1"
|
|
||||||
redux "^4.1.2"
|
|
||||||
|
|
||||||
doctrine@^2.1.0:
|
doctrine@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
|
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
|
||||||
@ -5149,24 +5125,6 @@ react-chartjs-2@4.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz#2a123df16d3a987c54eb4e810ed766d3c03adf8d"
|
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz#2a123df16d3a987c54eb4e810ed766d3c03adf8d"
|
||||||
integrity sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==
|
integrity sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==
|
||||||
|
|
||||||
react-dnd-html5-backend@15.1.3:
|
|
||||||
version "15.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.3.tgz#57b4f47e0f23923e7c243d2d0eefe490069115a9"
|
|
||||||
integrity sha512-HH/8nOEmrrcRGHMqJR91FOwhnLlx5SRLXmsQwZT3IPcBjx88WT+0pWC5A4tDOYDdoooh9k+KMPvWfxooR5TcOA==
|
|
||||||
dependencies:
|
|
||||||
dnd-core "15.1.2"
|
|
||||||
|
|
||||||
react-dnd@15.1.2:
|
|
||||||
version "15.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-15.1.2.tgz#211b30fd842326209c63f26f1bdf1bc52eef4f64"
|
|
||||||
integrity sha512-EaSbMD9iFJDY/o48T3c8wn3uWU+2uxfFojhesZN3LhigJoAIvH2iOjxofSA9KbqhAKP6V9P853G6XG8JngKVtA==
|
|
||||||
dependencies:
|
|
||||||
"@react-dnd/invariant" "3.0.1"
|
|
||||||
"@react-dnd/shallowequal" "3.0.1"
|
|
||||||
dnd-core "15.1.2"
|
|
||||||
fast-deep-equal "^3.1.3"
|
|
||||||
hoist-non-react-statics "^3.3.2"
|
|
||||||
|
|
||||||
react-dom@17.0.2:
|
react-dom@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||||
@ -5303,13 +5261,6 @@ redent@^3.0.0:
|
|||||||
indent-string "^4.0.0"
|
indent-string "^4.0.0"
|
||||||
strip-indent "^3.0.0"
|
strip-indent "^3.0.0"
|
||||||
|
|
||||||
redux@^4.1.2:
|
|
||||||
version "4.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
|
|
||||||
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.9.2"
|
|
||||||
|
|
||||||
reflect-metadata@0.1.13:
|
reflect-metadata@0.1.13:
|
||||||
version "0.1.13"
|
version "0.1.13"
|
||||||
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
|
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
|
||||||
|
Loading…
Reference in New Issue
Block a user