Update schedule view UI

This commit is contained in:
Dane Everitt 2020-10-14 20:13:36 -07:00
parent 33a43de723
commit f33d0b1d72
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
12 changed files with 230 additions and 134 deletions

View file

@ -1,9 +1,9 @@
{
"name": "pterodactyl-panel",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "1.2.19",
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"@fortawesome/react-fontawesome": "0.1.4",
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.11",
"axios": "^0.19.2",
"chart.js": "^2.8.0",
"codemirror": "^5.57.0",
@ -23,9 +23,9 @@
"react": "^16.13.1",
"react-dom": "npm:@hot-loader/react-dom",
"react-fast-compare": "^3.2.0",
"react-ga": "^3.1.2",
"react-google-recaptcha": "^2.0.1",
"react-helmet": "^6.1.0",
"react-ga": "^3.1.2",
"react-hot-loader": "^4.12.21",
"react-i18next": "^11.2.1",
"react-redux": "^7.1.0",

View file

@ -57,8 +57,8 @@ const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
`};
`};
${props => props.size === 'xsmall' && tw`p-2 text-xs`};
${props => (!props.size || props.size === 'small') && tw`p-3`};
${props => props.size === 'xsmall' && tw`px-2 py-1 text-xs`};
${props => (!props.size || props.size === 'small') && tw`px-4 py-2`};
${props => props.size === 'large' && tw`p-4 text-sm`};
${props => props.size === 'xlarge' && tw`p-4 w-full`};

View file

@ -0,0 +1,31 @@
import React, { CSSProperties } from 'react';
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
import tw from 'twin.macro';
interface Props {
icon: IconDefinition;
className?: string;
style?: CSSProperties;
}
const Icon = ({ icon, className, style }: Props) => {
let [ width, height, , , paths ] = icon.icon;
paths = Array.isArray(paths) ? paths : [ paths ];
return (
<svg
xmlns={'http://www.w3.org/2000/svg'}
viewBox={`0 0 ${width} ${height}`}
css={tw`fill-current inline-block`}
className={className}
style={style}
>
{paths.map((path, index) => (
<path key={`svg_path_${index}`} d={path}/>
))}
</svg>
);
};
export default Icon;

View file

@ -1,9 +1,10 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import Spinner from '@/components/elements/Spinner';
import tw from 'twin.macro';
import styled, { css } from 'styled-components/macro';
import { breakpoint } from '@/theme';
import Fade from '@/components/elements/Fade';
import { createPortal } from 'react-dom';
export interface RequiredModalProps {
visible: boolean;
@ -124,4 +125,10 @@ const Modal: React.FC<ModalProps> = ({ visible, appear, dismissable, showSpinner
);
};
export default Modal;
const PortaledModal: React.FC<ModalProps> = ({ children, ...props }) => {
const element = useRef(document.getElementById('modal-portal'));
return createPortal(<Modal {...props}>{children}</Modal>, element.current!);
};
export default PortaledModal;

View file

@ -49,7 +49,7 @@ export default ({ scheduleId, onDeleted }: Props) => {
Are you sure you want to delete this schedule? All tasks will be removed and any running processes
will be terminated.
</ConfirmationModal>
<Button css={tw`mr-4`} color={'red'} isSecondary onClick={() => setVisible(true)}>
<Button css={tw`flex-1 sm:flex-none mr-4 border-transparent`} color={'red'} isSecondary onClick={() => setVisible(true)}>
Delete
</Button>
</>

View file

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Schedule } from '@/api/server/schedules/getServerSchedules';
import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal';
import Button from '@/components/elements/Button';
import tw from 'twin.macro';
interface Props {
schedule: Schedule;
@ -18,7 +19,7 @@ export default ({ schedule }: Props) => {
onDismissed={() => setVisible(false)}
/>
}
<Button onClick={() => setVisible(true)}>
<Button onClick={() => setVisible(true)} css={tw`flex-1`}>
New Task
</Button>
</>

View file

@ -0,0 +1,35 @@
import React from 'react';
import tw from 'twin.macro';
import { Schedule } from '@/api/server/schedules/getServerSchedules';
interface Props {
cron: Schedule['cron'];
className?: string;
}
const ScheduleCronRow = ({ cron, className }: Props) => (
<div css={tw`flex`} className={className}>
<div css={tw`w-1/5 sm:w-auto text-center`}>
<p css={tw`font-medium`}>{cron.minute}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Minute</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{cron.hour}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Hour</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{cron.dayOfMonth}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Day (Month)</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>*</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Month</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{cron.dayOfWeek}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Day (Week)</p>
</div>
</div>
);
export default ScheduleCronRow;

View file

@ -1,12 +1,10 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { Schedule } from '@/api/server/schedules/getServerSchedules';
import getServerSchedule from '@/api/server/schedules/getServerSchedule';
import Spinner from '@/components/elements/Spinner';
import FlashMessageRender from '@/components/FlashMessageRender';
import { httpErrorToHuman } from '@/api/http';
import ScheduleRow from '@/components/server/schedules/ScheduleRow';
import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow';
import EditScheduleModal from '@/components/server/schedules/EditScheduleModal';
import NewTaskButton from '@/components/server/schedules/NewTaskButton';
import DeleteScheduleButton from '@/components/server/schedules/DeleteScheduleButton';
@ -16,7 +14,10 @@ import { ServerContext } from '@/state/server';
import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import GreyRowBox from '@/components/elements/GreyRowBox';
import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow';
import isEqual from 'react-fast-compare';
import { format } from 'date-fns';
import ScheduleCronRow from '@/components/server/schedules/ScheduleCronRow';
interface Params {
id: string;
@ -26,6 +27,24 @@ interface State {
schedule?: Schedule;
}
const CronBox = ({ title, value }: { title: string; value: string }) => (
<div css={tw`bg-neutral-700 rounded p-4`}>
<p css={tw`text-neutral-300 text-sm`}>{title}</p>
<p css={tw`text-2xl font-medium text-neutral-100`}>{value}</p>
</div>
);
const ActivePill = ({ active }: { active: boolean }) => (
<span
css={[
tw`rounded-full px-2 py-px text-xs ml-4 uppercase`,
active ? tw`bg-green-600 text-green-100` : tw`bg-red-600 text-red-100`,
]}
>
{active ? 'Active' : 'Inactive'}
</span>
);
export default ({ match, history, location: { state } }: RouteComponentProps<Params, Record<string, unknown>, State>) => {
const id = ServerContext.useStoreState(state => state.server.data!.id);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
@ -34,7 +53,8 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
const [ isLoading, setIsLoading ] = useState(true);
const [ showEditModal, setShowEditModal ] = useState(false);
const schedule = ServerContext.useStoreState(st => st.schedules.data.find(s => s.id === state.schedule?.id), [ match ]);
// @ts-ignore
const schedule: Schedule | undefined = ServerContext.useStoreState(st => st.schedules.data.find(s => s.id === state.schedule?.id), isEqual);
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
useEffect(() => {
@ -53,6 +73,10 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
.then(() => setIsLoading(false));
}, [ match ]);
const toggleEditModal = useCallback(() => {
setShowEditModal(s => !s);
}, []);
return (
<PageContentBlock>
<FlashMessageRender byKey={'schedules'} css={tw`mb-4`}/>
@ -60,52 +84,68 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
<Spinner size={'large'} centered/>
:
<>
<GreyRowBox css={tw`cursor-pointer mb-2 flex-wrap`}>
<ScheduleRow schedule={schedule}/>
</GreyRowBox>
<EditScheduleModal
visible={showEditModal}
schedule={schedule}
onDismissed={() => setShowEditModal(false)}
/>
<div css={tw`flex items-center mt-8 mb-4`}>
<ScheduleCronRow cron={schedule.cron} css={tw`sm:hidden bg-neutral-700 rounded mb-4 p-3`}/>
<div css={tw`hidden sm:grid grid-cols-5 md:grid-cols-7 gap-4 mb-6`}>
<CronBox title={'Minute'} value={schedule.cron.minute}/>
<CronBox title={'Hour'} value={schedule.cron.hour}/>
<CronBox title={'Day (Month)'} value={schedule.cron.dayOfMonth}/>
<CronBox title={'Month'} value={'*'}/>
<CronBox title={'Day (Week)'} value={schedule.cron.dayOfWeek}/>
</div>
<div css={tw`rounded shadow`}>
<div css={tw`sm:flex items-center bg-neutral-900 p-3 sm:p-6 border-b-4 border-neutral-600 rounded-t`}>
<div css={tw`flex-1`}>
<h2 css={tw`text-2xl`}>Configured Tasks</h2>
</div>
</div>
{schedule.tasks.length > 0 ?
<>
{
schedule.tasks
.sort((a, b) => a.sequenceId - b.sequenceId)
.map(task => (
<ScheduleTaskRow key={task.id} task={task} schedule={schedule}/>
))
}
{schedule.tasks.length > 1 &&
<p css={tw`text-xs text-neutral-400`}>
Task delays are relative to the previous task in the listing.
</p>
}
</>
<h3 css={tw`flex items-center text-neutral-100 text-2xl`}>
{schedule.name}
{schedule.isProcessing ?
<span
css={tw`flex items-center rounded-full px-2 py-px text-xs ml-4 uppercase bg-neutral-600 text-white`}
>
<Spinner css={tw`w-3! h-3! mr-2`}/>
Processing
</span>
:
<p css={tw`text-sm text-neutral-400`}>
There are no tasks configured for this schedule.
</p>
<ActivePill active={schedule.isActive}/>
}
<div css={tw`mt-8 flex justify-end`}>
</h3>
<p css={tw`mt-1 text-sm text-neutral-300`}>
Last run at:&nbsp;
{schedule.lastRunAt ? format(schedule.lastRunAt, 'MMM do \'at\' h:mma') : 'never'}
</p>
</div>
<div css={tw`flex sm:block mt-3 sm:mt-0`}>
<Can action={'schedule.update'}>
<Button
isSecondary
color={'grey'}
size={'small'}
css={tw`flex-1 mr-4 border-transparent`}
onClick={toggleEditModal}
>
Edit
</Button>
<NewTaskButton schedule={schedule}/>
</Can>
</div>
</div>
<div css={tw`bg-neutral-700 rounded-b`}>
{schedule.tasks.length > 0 ?
schedule.tasks.map(task => (
<ScheduleTaskRow key={`${schedule.id}_${task.id}`} task={task} schedule={schedule}/>
))
:
null
}
</div>
</div>
<EditScheduleModal visible={showEditModal} schedule={schedule} onDismissed={toggleEditModal}/>
<div css={tw`mt-6 flex sm:justify-end`}>
<Can action={'schedule.delete'}>
<DeleteScheduleButton
scheduleId={schedule.id}
onDeleted={() => history.push(`/server/${id}/schedules`)}
/>
</Can>
<Can action={'schedule.update'}>
<Button css={tw`mr-4`} onClick={() => setShowEditModal(true)}>
Edit
</Button>
<NewTaskButton schedule={schedule}/>
</Can>
</div>
</>
}

View file

@ -4,6 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCalendarAlt } from '@fortawesome/free-solid-svg-icons';
import { format } from 'date-fns';
import tw from 'twin.macro';
import ScheduleCronRow from '@/components/server/schedules/ScheduleCronRow';
export default ({ schedule }: { schedule: Schedule }) => (
<>
@ -27,36 +28,19 @@ export default ({ schedule }: { schedule: Schedule }) => (
{schedule.isActive ? 'Active' : 'Inactive'}
</p>
</div>
<div css={tw`flex items-center mx-auto sm:mx-8 w-full sm:w-auto mt-4 sm:mt-0`}>
<div css={tw`w-1/5 sm:w-auto text-center`}>
<p css={tw`font-medium`}>{schedule.cron.minute}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Minute</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{schedule.cron.hour}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Hour</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{schedule.cron.dayOfMonth}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Day (Month)</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>*</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Month</p>
</div>
<div css={tw`w-1/5 sm:w-auto text-center ml-4`}>
<p css={tw`font-medium`}>{schedule.cron.dayOfWeek}</p>
<p css={tw`text-2xs text-neutral-500 uppercase`}>Day (Week)</p>
</div>
</div>
<ScheduleCronRow cron={schedule.cron} css={tw`mx-auto sm:mx-8 w-full sm:w-auto mt-4 sm:mt-0`}/>
<div>
<p
css={[
tw`py-1 px-3 rounded text-xs uppercase text-white hidden sm:block`,
schedule.isActive ? tw`bg-green-600` : tw`bg-neutral-400`,
schedule.isActive && !schedule.isProcessing ? tw`bg-green-600` : tw`bg-neutral-400`,
]}
>
{schedule.isActive ? 'Active' : 'Inactive'}
{schedule.isProcessing ?
'Processing'
:
schedule.isActive ? 'Active' : 'Inactive'
}
</p>
</div>
</>

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCode, faFileArchive, faPencilAlt, faToggleOn, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import { faClock, faCode, faFileArchive, faPencilAlt, faToggleOn, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask';
import { httpErrorToHuman } from '@/api/http';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
@ -11,6 +11,7 @@ import useFlash from '@/plugins/useFlash';
import { ServerContext } from '@/state/server';
import tw from 'twin.macro';
import ConfirmationModal from '@/components/elements/ConfirmationModal';
import Icon from '@/components/elements/Icon';
interface Props {
schedule: Schedule;
@ -56,7 +57,7 @@ export default ({ schedule, task }: Props) => {
const [ title, icon ] = getActionDetails(task.action);
return (
<div css={tw`flex flex-wrap items-center bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded`}>
<div css={tw`sm:flex items-center p-3 sm:p-6 border-b border-neutral-800`}>
<SpinnerOverlay visible={isLoading} fixed size={'large'}/>
{isEditing && <TaskDetailsModal
schedule={schedule}
@ -73,8 +74,8 @@ export default ({ schedule, task }: Props) => {
Are you sure you want to delete this task? This action cannot be undone.
</ConfirmationModal>
<FontAwesomeIcon icon={icon} css={tw`text-lg text-white hidden md:block`}/>
<div css={tw`flex-none sm:flex-1 mb-4 sm:mb-0 w-full md:w-auto overflow-x-auto`}>
<p css={tw`md:ml-6 text-neutral-300 uppercase text-xs`}>
<div css={tw`flex-none sm:flex-1 w-full sm:w-auto overflow-x-auto`}>
<p css={tw`md:ml-6 text-neutral-200 uppercase text-sm`}>
{title}
</p>
{task.payload &&
@ -87,14 +88,13 @@ export default ({ schedule, task }: Props) => {
</div>
}
</div>
{task.sequenceId > 1 &&
<div css={tw`mt-3 sm:mt-0 flex items-center w-full sm:w-auto`}>
{task.sequenceId > 1 && task.timeOffset > 0 &&
<div css={tw`mr-6`}>
<p css={tw`text-center mb-1`}>
{task.timeOffset}s
</p>
<p css={tw`text-neutral-300 uppercase text-2xs`}>
Delay Run By
</p>
<div css={tw`flex items-center px-2 py-1 bg-neutral-500 text-sm rounded-full`}>
<Icon icon={faClock} css={tw`w-3 h-3 mr-2`}/>
{task.timeOffset}s later
</div>
</div>
}
<Can action={'schedule.update'}>
@ -118,5 +118,6 @@ export default ({ schedule, task }: Props) => {
</button>
</Can>
</div>
</div>
);
};

View file

@ -3,5 +3,6 @@
])
@section('container')
<div id="modal-portal"></div>
<div id="app"></div>
@endsection

View file

@ -888,30 +888,31 @@
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
"@fortawesome/fontawesome-common-types@^0.2.19":
version "0.2.19"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.19.tgz#754a0f85e1290858152e1c05700ab502b11197f1"
"@fortawesome/fontawesome-common-types@^0.2.32":
version "0.2.32"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.32.tgz#3436795d5684f22742989bfa08f46f50f516f259"
integrity sha512-ux2EDjKMpcdHBVLi/eWZynnPxs0BtFVXJkgHIxXRl+9ZFaHPvYamAfCzeeQFqHRjuJtX90wVnMRaMQAAlctz3w==
"@fortawesome/fontawesome-svg-core@1.2.19":
version "1.2.19"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.19.tgz#0eca1ce9285c3d99e6e340633ee8f615f9d1a2e0"
integrity sha512-D4ICXg9oU08eF9o7Or392gPpjmwwgJu8ecCFusthbID95CLVXOgIyd4mOKD9Nud5Ckz+Ty59pqkNtThDKR0erA==
"@fortawesome/fontawesome-svg-core@^1.2.32":
version "1.2.32"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.32.tgz#da092bfc7266aa274be8604de610d7115f9ba6cf"
integrity sha512-XjqyeLCsR/c/usUpdWcOdVtWFVjPbDFBTQkn2fQRrWhhUoxriQohO2RWDxLyUM8XpD+Zzg5xwJ8gqTYGDLeGaQ==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.19"
"@fortawesome/fontawesome-common-types" "^0.2.32"
"@fortawesome/free-solid-svg-icons@^5.9.0":
version "5.9.0"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.9.0.tgz#1c73e7bac17417d23f934d83f7fff5b100a7fda9"
"@fortawesome/free-solid-svg-icons@^5.15.1":
version "5.15.1"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.1.tgz#e1432676ddd43108b41197fee9f86d910ad458ef"
integrity sha512-EFMuKtzRMNbvjab/SvJBaOOpaqJfdSap/Nl6hst7CgrJxwfORR1drdTV6q1Ib/JVzq4xObdTDcT6sqTaXMqfdg==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.19"
"@fortawesome/fontawesome-common-types" "^0.2.32"
"@fortawesome/react-fontawesome@0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.4.tgz#18d61d9b583ca289a61aa7dccc05bd164d6bc9ad"
integrity sha512-GwmxQ+TK7PEdfSwvxtGnMCqrfEm0/HbRHArbUudsYiy9KzVCwndxa2KMcfyTQ8El0vROrq8gOOff09RF1oQe8g==
"@fortawesome/react-fontawesome@^0.1.11":
version "0.1.11"
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.11.tgz#c1a95a2bdb6a18fa97b355a563832e248bf6ef4a"
integrity sha512-sClfojasRifQKI0OPqTy8Ln8iIhnxR/Pv/hukBhWnBz9kQRmqi6JSH3nghlhAY7SUeIIM7B5/D2G8WjX0iepVg==
dependencies:
humps "^2.0.1"
prop-types "^15.5.10"
prop-types "^15.7.2"
"@fullhuman/postcss-purgecss@^2.1.2":
version "2.3.0"
@ -3767,11 +3768,6 @@ https-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
humps@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa"
integrity sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao=
i18next-chained-backend@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/i18next-chained-backend/-/i18next-chained-backend-2.0.0.tgz#faf2e8b5f081a01e74fbec1fe580c184bc64e25b"
@ -5416,7 +5412,7 @@ promise-inflight@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==