Google calendar widget

Signed-off-by: Alex Rybchenko <arybchenko@box.com>
This commit is contained in:
Alex Rybchenko
2022-02-21 17:47:06 +01:00
parent 5dcf41492f
commit 90bc58b9b9
36 changed files with 2286 additions and 8 deletions
+2 -1
View File
@@ -29,6 +29,7 @@
"@backstage/plugin-code-coverage": "^0.1.27",
"@backstage/plugin-cost-insights": "^0.11.22",
"@backstage/plugin-explore": "^0.3.31",
"@backstage/plugin-gcalendar-homepage": "^0.0.0",
"@backstage/plugin-gcp-projects": "^0.3.19",
"@backstage/plugin-github-actions": "^0.5.0",
"@backstage/plugin-gocd": "^0.1.6",
@@ -59,10 +60,10 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"@octokit/rest": "^18.5.3",
"@roadiehq/backstage-plugin-buildkite": "^1.3.8",
"@roadiehq/backstage-plugin-github-insights": "^1.5.0",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.4.0",
"@roadiehq/backstage-plugin-travis-ci": "^1.3.6",
"@roadiehq/backstage-plugin-buildkite": "^1.3.8",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^17.0.2",
+11 -1
View File
@@ -33,7 +33,12 @@ import {
createApiFactory,
errorApiRef,
githubAuthApiRef,
googleAuthApiRef,
} from '@backstage/core-plugin-api';
import {
GCalendarApiClient,
gcalendarApiRef,
} from '@backstage/plugin-gcalendar-homepage';
export const apis: AnyApiFactory[] = [
createApiFactory({
@@ -41,9 +46,14 @@ export const apis: AnyApiFactory[] = [
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
ScmAuth.createDefaultApiFactory(),
createApiFactory({
api: gcalendarApiRef,
deps: { authApi: googleAuthApiRef },
factory: deps => new GCalendarApiClient(deps),
}),
createApiFactory({
api: graphQlBrowseApiRef,
deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef },
@@ -25,6 +25,7 @@ import {
} from '@backstage/plugin-home';
import { Content, Header, Page } from '@backstage/core-components';
import { HomePageSearchBar } from '@backstage/plugin-search';
import { CalendarCard } from '@backstage/plugin-gcalendar-homepage';
import Grid from '@material-ui/core/Grid';
import React from 'react';
@@ -100,6 +101,9 @@ export const HomePage = () => (
]}
/>
</Grid>
<Grid item xs={12} md={4}>
<CalendarCard />
</Grid>
</Grid>
</Content>
</Page>
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+13
View File
@@ -0,0 +1,13 @@
# gcalendar-homepage
Welcome to the gcalendar-homepage plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/gcalendar-homepage](http://localhost:3000/gcalendar-homepage).
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { gcalendarHomepagePlugin } from '../src/plugin';
createDevApp().registerPlugin(gcalendarHomepagePlugin).render();
+64
View File
@@ -0,0 +1,64 @@
{
"name": "@backstage/plugin-gcalendar-homepage",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core-components": "^0.8.9",
"@backstage/core-plugin-api": "^0.6.1",
"@backstage/dev-utils": "^0.2.22",
"@backstage/theme": "^0.2.15",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"@testing-library/jest-dom": "^5.16.2",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"cross-fetch": "^3.1.5",
"lodash": "^4.17.21",
"luxon": "^2.3.0",
"material-ui-popup-state": "^2.0.0",
"react-query": "^3.34.16",
"react-use": "^17.2.4",
"sanitize-html": "^2.7.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.14.0",
"@backstage/core-app-api": "^0.5.3",
"@backstage/dev-utils": "^0.2.22",
"@backstage/test-utils": "^0.2.5",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/gapi": "^0.0.41",
"@types/gapi.auth2": "^0.0.56",
"@types/gapi.client.calendar": "^3.0.10",
"@types/jest": "*",
"@types/node": "*",
"@types/sanitize-html": "^2.6.2",
"cross-fetch": "^3.1.5",
"msw": "^0.35.0"
},
"files": [
"dist"
]
}
@@ -0,0 +1,71 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import axios, { AxiosInstance } from 'axios';
import { OAuthApi, createApiRef } from '@backstage/core-plugin-api';
import { GCalendar, GCalendarEvent } from '../components/CalendarCard/types';
type Options = {
authApi: OAuthApi;
};
export const gcalendarApiRef = createApiRef<GCalendarApiClient>({
id: 'plugin.gcalendar.service',
});
export class GCalendarApiClient {
private readonly authApi: OAuthApi;
private readonly http: AxiosInstance;
constructor(options: Options) {
this.authApi = options.authApi;
this.http = axios.create({
baseURL: 'https://www.googleapis.com/calendar/v3',
});
this.http.interceptors.request.use(async config => {
const token = await this.authApi.getAccessToken();
if (!config.headers) {
config.headers = {};
}
config.headers.Authorization = `Bearer ${token}`;
return config;
});
}
public async getCalendars(params?: any): Promise<{ items: GCalendar[] }> {
const { data } = await this.http.get('/users/me/calendarList', {
params,
});
return data;
}
public async getEvents(
calendarId: string,
params?: any,
): Promise<{ items: GCalendarEvent[] }> {
const { data } = await this.http.get(
`/calendars/${encodeURIComponent(calendarId)}/events`,
{
params,
},
);
return data;
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { AttendeeChip } from './AttendeeChip';
import { EventAttendee, ResponseStatus } from './types';
describe('<AttendeeChip />', () => {
it('renders attendee email', async () => {
const email = 'test@test.com';
const user: EventAttendee = {
email,
responseStatus: ResponseStatus.needsAction,
};
const { queryByText } = await renderInTestApp(<AttendeeChip user={user} />);
expect(queryByText(email)).toBeInTheDocument();
});
it('renders accepted icon', async () => {
const email = 'test@test.com';
const user: EventAttendee = {
email,
responseStatus: ResponseStatus.accepted,
};
const { getByTestId } = await renderInTestApp(<AttendeeChip user={user} />);
expect(getByTestId('accepted-icon')).toBeInTheDocument();
});
it('renders declined icon', async () => {
const email = 'test@test.com';
const user: EventAttendee = {
email,
responseStatus: ResponseStatus.declined,
};
const { getByTestId } = await renderInTestApp(<AttendeeChip user={user} />);
expect(getByTestId('declined-icon')).toBeInTheDocument();
});
});
@@ -0,0 +1,87 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { BackstageTheme } from '@backstage/theme';
import { Badge, Chip, makeStyles } from '@material-ui/core';
import CancelIcon from '@material-ui/icons/Cancel';
import CheckIcon from '@material-ui/icons/CheckCircle';
import { EventAttendee, ResponseStatus } from './types';
const useStyles = makeStyles((theme: BackstageTheme) => {
const getIconColor = (responseStatus?: string) => {
if (!responseStatus) return theme.palette.primary.light;
return {
[ResponseStatus.accepted]: theme.palette.status.ok,
[ResponseStatus.declined]: theme.palette.status.error,
}[responseStatus];
};
return {
responseStatus: {
color: ({ responseStatus }: { responseStatus?: string }) =>
getIconColor(responseStatus),
},
badge: {
right: 10,
top: 5,
'& svg': {
height: 16,
width: 16,
background: '#fff',
},
},
};
});
const ResponseIcon = ({ responseStatus }: any) => {
if (responseStatus === ResponseStatus.accepted) {
return <CheckIcon data-testid="accepted-icon" />;
}
if (responseStatus === ResponseStatus.declined) {
return <CancelIcon data-testid="declined-icon" />;
}
return null;
};
type AttendeeChipProps = {
user: EventAttendee;
};
export const AttendeeChip = ({ user }: AttendeeChipProps) => {
const classes = useStyles({ responseStatus: user.responseStatus });
return (
<Badge
classes={{
root: classes.responseStatus,
badge: classes.badge,
}}
badgeContent={<ResponseIcon responseStatus={user.responseStatus} />}
>
<Chip
size="small"
variant="outlined"
label={user.email}
color="primary"
/>
</Badge>
);
};
@@ -0,0 +1,155 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { googleAuthApiRef, storageApiRef } from '@backstage/core-plugin-api';
import {
MockStorageApi,
TestApiProvider,
renderInTestApp,
} from '@backstage/test-utils';
import { CalendarCardContainer } from '.';
import { gcalendarApiRef, gcalendarHomepagePlugin } from '../..';
describe('<CalendarCard />', () => {
const primaryCalendar = {
id: 'test-1@test.com',
summary: 'test-1@test.com',
primary: true,
};
const nonPrimaryCalendar = {
id: 'test-2@test.com',
summary: 'test-2@test.com',
primary: false,
};
const calendarData = {
items: [primaryCalendar, nonPrimaryCalendar],
};
const eventData = {
items: [
{
id: '1',
summary: 'Test event',
},
{
id: '2',
summary: 'Test event',
},
],
};
const getMockedApi = (calendarMock = {}, eventMock = {}) => ({
getCalendars: jest.fn().mockResolvedValue(calendarMock),
getEvents: jest.fn().mockResolvedValue(eventMock),
});
const getAuthMockApi = (token = 'token') => ({
getAccessToken: jest.fn().mockResolvedValue(token),
});
const mockStorage = MockStorageApi.create();
it('should render "Sign in" button if user not signed in', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[gcalendarApiRef, getMockedApi()],
[googleAuthApiRef, getAuthMockApi('')],
]}
>
<CalendarCardContainer />
</TestApiProvider>,
);
expect(rendered.queryByText('Sign in')).toBeInTheDocument();
});
it('should render empty card', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[gcalendarApiRef, getMockedApi()],
[googleAuthApiRef, getAuthMockApi()],
]}
>
<CalendarCardContainer />
</TestApiProvider>,
);
expect(rendered.queryByText('No events')).toBeInTheDocument();
expect(rendered.queryByText('Go to Calendar')).toBeInTheDocument();
});
it('should select primary calendar by default', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[gcalendarApiRef, getMockedApi(calendarData, eventData)],
[googleAuthApiRef, getAuthMockApi()],
]}
>
<CalendarCardContainer />
</TestApiProvider>,
);
expect(rendered.queryByText(primaryCalendar.summary)).toBeInTheDocument();
expect(
rendered.queryByText(nonPrimaryCalendar.summary),
).not.toBeInTheDocument();
});
it('should render calendar events', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[gcalendarApiRef, getMockedApi(calendarData, eventData)],
[googleAuthApiRef, getAuthMockApi()],
]}
>
<CalendarCardContainer />
</TestApiProvider>,
);
expect(rendered.queryByText('No events')).not.toBeInTheDocument();
expect(rendered.queryAllByText('Test event')).toHaveLength(2);
});
it('should select stored calendar', async () => {
mockStorage
.forBucket(gcalendarHomepagePlugin.getId())
.set('google_calendars_selected', [nonPrimaryCalendar.id]);
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[gcalendarApiRef, getMockedApi(calendarData, eventData)],
[googleAuthApiRef, getAuthMockApi()],
[storageApiRef, mockStorage],
]}
>
<CalendarCardContainer />
</TestApiProvider>,
);
expect(
rendered.queryByText(nonPrimaryCalendar.summary),
).toBeInTheDocument();
expect(
rendered.queryByText(primaryCalendar.summary),
).not.toBeInTheDocument();
});
});
@@ -0,0 +1,146 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { sortBy } from 'lodash';
import { DateTime } from 'luxon';
import React, { useEffect, useMemo, useState } from 'react';
import { InfoCard, Progress } from '@backstage/core-components';
import { useAnalytics } from '@backstage/core-plugin-api';
import { Box, IconButton, Typography } from '@material-ui/core';
import PrevIcon from '@material-ui/icons/NavigateBefore';
import NextIcon from '@material-ui/icons/NavigateNext';
import {
useCalendarsQuery,
useEventsQuery,
useSignIn,
useStoredCalendars,
} from '../../hooks';
import calendarCardIcon from '../../icons/calendarIcon.svg';
import { CalendarEvent } from './CalendarEvent';
import { CalendarSelect } from './CalendarSelect';
import { SignInContent } from './SignInContent';
import { getStartDate } from './util';
export const CalendarCard = () => {
const [date, setDate] = useState(DateTime.now());
const analytics = useAnalytics();
const changeDay = (offset = 1) => {
setDate(prev => prev.plus({ day: offset }));
analytics.captureEvent('click', 'change date');
};
const { isSignedIn, isInitialized, signIn } = useSignIn();
useEffect(() => {
signIn(true);
}, [signIn]);
const { isLoading: isCalendarLoading, data } = useCalendarsQuery({
enabled: isSignedIn,
});
const calendars = useMemo(() => data?.items || [], [data]);
const primaryCalendarId = calendars.find(c => c.primary === true)?.id;
const defaultSelectedCalendars = primaryCalendarId ? [primaryCalendarId] : [];
const [storedCalendars, setStoredCalendars] = useStoredCalendars(
defaultSelectedCalendars,
);
const { events, isLoading: isEventLoading } = useEventsQuery({
calendars,
selectedCalendars: storedCalendars,
enabled: isSignedIn && calendars.length > 0,
timeMin: date.startOf('day').toISO(),
timeMax: date.endOf('day').toISO(),
timeZone: date.zoneName,
});
return (
<InfoCard
noPadding
title={
<Box display="flex" alignItems="center">
<Box height={24} width={24} mr={1}>
<img src={calendarCardIcon} alt="Google Calendar" />
</Box>
{isSignedIn ? (
<>
<IconButton onClick={() => changeDay(-1)} size="small">
<PrevIcon />
</IconButton>
<IconButton onClick={() => changeDay(1)} size="small">
<NextIcon />
</IconButton>
<Box mr={0.5} />
<Typography variant="h6">
{date.toLocaleString({
weekday: 'short',
month: 'short',
day: 'numeric',
})}
</Typography>
<Box flex={1} />
<CalendarSelect
calendars={calendars}
selectedCalendars={storedCalendars}
setSelectedCalendars={setStoredCalendars}
disabled={isCalendarLoading || !isSignedIn}
/>
</>
) : (
<Typography variant="h6">Agenda</Typography>
)}
</Box>
}
deepLink={{
link: 'https://calendar.google.com/',
title: 'Go to Calendar',
}}
>
<Box>
{(isCalendarLoading || !isInitialized || isEventLoading) && (
<Box pt={2} pb={2}>
<Progress variant="query" />
</Box>
)}
{!isSignedIn && isInitialized && (
<SignInContent handleAuthClick={() => signIn(false)} />
)}
{!isEventLoading && !isCalendarLoading && isSignedIn && (
<Box p={1} pb={0} maxHeight={602} overflow="auto">
{events.length === 0 && (
<Box pt={2} pb={2}>
<Typography align="center" variant="h6">
No events
</Typography>
</Box>
)}
{sortBy(events, [getStartDate]).map(event => (
<CalendarEvent
key={`${event.calendarId}-${event.id}`}
event={event}
/>
))}
</Box>
)}
</Box>
</InfoCard>
);
};
@@ -0,0 +1,29 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { CalendarCard } from './CalendarCard';
const queryClient = new QueryClient();
export const CalendarCardContainer = () => {
return (
<QueryClientProvider client={queryClient}>
<CalendarCard />
</QueryClientProvider>
);
};
@@ -0,0 +1,83 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { fireEvent } from '@testing-library/react';
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { CalendarEvent } from './CalendarEvent';
describe('<CalendarEvent />', () => {
const event = {
summary: 'Test event',
htmlLink: '/calendar-link',
start: {
dateTime: '2022-02-09T10:15:00',
},
end: {
dateTime: '2022-02-09T10:45:00',
},
conferenceData: {
entryPoints: [{ entryPointType: 'video', uri: '/zoom-link' }],
},
description: 'Test description',
attendees: [
{
email: 'test@test.com',
},
],
};
it('should render calendar event', async () => {
const { queryByText, queryByTestId } = await renderInTestApp(
<CalendarEvent event={event} />,
);
expect(queryByText(event.summary)).toBeInTheDocument();
expect(queryByTestId('calendar-event-zoom-link')).toBeInTheDocument();
expect(queryByTestId('calendar-event-zoom-link')).toHaveAttribute(
'href',
event.conferenceData.entryPoints[0].uri,
);
expect(queryByTestId('calendar-event-time')).toBeInTheDocument();
});
it('should not render time for events longer than 1 day', async () => {
const allDayEvent = {
summary: 'Test event',
start: {
date: '2022-02-09',
},
end: {
date: '2022-02-19',
},
};
const { queryByText, queryByTestId } = await renderInTestApp(
<CalendarEvent event={allDayEvent} />,
);
expect(queryByText(allDayEvent.summary)).toBeInTheDocument();
expect(queryByTestId('calendar-event-time')).not.toBeInTheDocument();
});
it('should show popover on click', async () => {
const { queryByTestId, getByTestId } = await renderInTestApp(
<CalendarEvent event={event} />,
);
expect(queryByTestId('calendar-event-popover')).not.toBeInTheDocument();
fireEvent.click(getByTestId('calendar-event'));
expect(queryByTestId('calendar-event-popover')).toBeInTheDocument();
});
});
@@ -0,0 +1,160 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classnames from 'classnames';
import {
bindPopover,
bindTrigger,
usePopupState,
} from 'material-ui-popup-state/hooks';
import React, { useState } from 'react';
import { useAnalytics } from '@backstage/core-plugin-api';
import {
Box,
Link,
Paper,
Popover,
Tooltip,
Typography,
makeStyles,
} from '@material-ui/core';
import zoomIcon from '../../icons/zoomIcon.svg';
import { CalendarEventPopoverContent } from './CalendarEventPopoverContent';
import { GCalendarEvent, ResponseStatus } from './types';
import { getTimePeriod, getZoomLink, isAllDay, isPassed } from './util';
const useStyles = makeStyles(theme => ({
event: {
display: 'flex',
alignItems: 'center',
marginBottom: theme.spacing(1),
cursor: 'pointer',
paddingRight: 12,
},
declined: {
textDecoration: 'line-through',
},
passed: {
opacity: 0.6,
transition: 'opacity 0.15s ease-in-out',
'&:hover': {
opacity: 1,
},
},
link: {
width: 48,
height: 48,
display: 'inline-block',
padding: 8,
borderRadius: '50%',
'&:hover': {
backgroundColor: theme.palette.grey[100],
},
},
calendarColor: ({ event }: any) => ({
width: 8,
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
backgroundColor: event.primary
? theme.palette.primary.light
: event.backgroundColor,
}),
}));
export const CalendarEvent = ({ event }: { event: GCalendarEvent }) => {
const classes = useStyles({ event });
const popoverState = usePopupState({
variant: 'popover',
popupId: event.id,
disableAutoFocus: true,
});
const [hovered, setHovered] = useState(false);
const analytics = useAnalytics();
const zoomLink = getZoomLink(event);
const { onClick, ...restBindProps } = bindTrigger(popoverState);
return (
<>
<Paper
onClick={e => {
onClick(e);
analytics.captureEvent('click', 'event info');
}}
{...restBindProps}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
elevation={hovered ? 4 : 1}
className={classnames(classes.event, {
[classes.passed]: isPassed(event),
})}
data-testid="calendar-event"
>
<Box className={classes.calendarColor} mr={1} alignSelf="stretch" />
<Box flex={1} pt={1} pb={1}>
<Typography
variant="subtitle2"
className={classnames({
[classes.declined]:
event.responseStatus === ResponseStatus.declined,
})}
>
{event.summary}
</Typography>
{!isAllDay(event) && (
<Typography variant="body2" data-testid="calendar-event-time">
{getTimePeriod(event)}
</Typography>
)}
</Box>
{zoomLink && (
<Tooltip title="Join Zoom Meeting">
<Link
data-testid="calendar-event-zoom-link"
className={classes.link}
href={zoomLink}
target="_blank"
onClick={e => {
e.stopPropagation();
analytics.captureEvent('click', 'zoom link');
}}
>
<img src={zoomIcon} alt="Zoom link" />
</Link>
</Tooltip>
)}
</Paper>
<Popover
{...bindPopover(popoverState)}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
data-testid="calendar-event-popover"
>
<CalendarEventPopoverContent event={event} />
</Popover>
</>
);
};
@@ -0,0 +1,55 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { CalendarEventPopoverContent } from './CalendarEventPopoverContent';
describe('<CalendarEventPopoverContent />', () => {
const event = {
summary: 'Test event',
htmlLink: '/calendar-link',
conferenceData: {
entryPoints: [{ entryPointType: 'video', uri: '/zoom-link' }],
},
description: 'Test description',
attendees: [
{
email: 'test@test.com',
},
],
};
it('should render event info', async () => {
const { queryByText, queryByTestId } = await renderInTestApp(
<CalendarEventPopoverContent event={event} />,
);
expect(queryByText(event.summary)).toBeInTheDocument();
expect(queryByText(event.description)).toBeInTheDocument();
expect(queryByText(event.attendees[0].email)).toBeInTheDocument();
expect(queryByText('Join Zoom Meeting')).toBeInTheDocument();
expect(queryByText('Join Zoom Meeting')?.closest('a')).toHaveAttribute(
'href',
event.conferenceData.entryPoints[0].uri,
);
expect(queryByTestId('open-calendar-link')).toHaveAttribute(
'href',
event.htmlLink,
);
expect(queryByText(event.attendees[0].email)).toBeInTheDocument();
});
});
@@ -0,0 +1,124 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { sortBy } from 'lodash';
import React from 'react';
import sanitizeHtml from 'sanitize-html';
import { useAnalytics } from '@backstage/core-plugin-api';
import {
Box,
Divider,
IconButton,
Link,
Tooltip,
Typography,
makeStyles,
} from '@material-ui/core';
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
import { AttendeeChip } from './AttendeeChip';
import { GCalendarEvent } from './types';
import { getTimePeriod, getZoomLink } from './util';
const useStyles = makeStyles(theme => {
return {
description: {
wordBreak: 'break-word',
'& a': {
color: theme.palette.primary.main,
fontWeight: 500,
},
},
divider: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
};
});
type CalendarEventPopoverProps = {
event: GCalendarEvent;
};
export const CalendarEventPopoverContent = ({
event,
}: CalendarEventPopoverProps) => {
const classes = useStyles({ event });
const analytics = useAnalytics();
const zoomLink = getZoomLink(event);
return (
<Box display="flex" flexDirection="column" width={400} p={2}>
<Box display="flex" alignItems="center">
<Box flex={1}>
<Typography variant="h6">{event.summary}</Typography>
<Typography variant="subtitle2">{getTimePeriod(event)}</Typography>
</Box>
{event.htmlLink && (
<Tooltip title="Open in Calendar">
<Link
data-testid="open-calendar-link"
href={event.htmlLink}
target="_blank"
onClick={_e =>
analytics.captureEvent('click', 'open in calendar')
}
>
<IconButton>
<ArrowForwardIcon />
</IconButton>
</Link>
</Tooltip>
)}
</Box>
{zoomLink && (
<Link
href={zoomLink}
target="_blank"
onClick={_e => analytics.captureEvent('click', 'zoom link')}
>
Join Zoom Meeting
</Link>
)}
{event.description && (
<>
<Divider className={classes.divider} variant="fullWidth" />
<Box
className={classes.description}
dangerouslySetInnerHTML={{
__html: sanitizeHtml(event.description),
}}
/>
</>
)}
{event.attendees && (
<>
<Divider className={classes.divider} variant="fullWidth" />
<Box>
<Typography variant="subtitle2">Attendees</Typography>
<Box mb={1} />
{sortBy(event.attendees || [], 'email').map(user => (
<AttendeeChip key={user.email} user={user} />
))}
</Box>
</>
)}
</Box>
);
};
@@ -0,0 +1,91 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { sortBy } from 'lodash';
import React from 'react';
import {
Checkbox,
FormControl,
Input,
ListItemText,
MenuItem,
Select,
Typography,
makeStyles,
} from '@material-ui/core';
import { GCalendar } from './types';
const useStyles = makeStyles({
formControl: {
width: 120,
},
selectedCalendars: {
textOverflow: 'ellipsis',
overflow: 'hidden',
},
});
type CalendarSelectProps = {
disabled: boolean;
selectedCalendars?: string[];
setSelectedCalendars: (value: string[]) => void;
calendars: GCalendar[];
};
export const CalendarSelect = ({
disabled,
selectedCalendars = [],
setSelectedCalendars,
calendars,
}: CalendarSelectProps) => {
const classes = useStyles();
return (
<FormControl className={classes.formControl}>
<Select
labelId="calendars-label"
disabled={disabled || calendars.length === 0}
multiple
value={selectedCalendars}
onChange={async e => setSelectedCalendars(e.target.value as string[])}
input={<Input />}
renderValue={selected => (
<Typography className={classes.selectedCalendars} variant="body2">
{calendars
.filter(c => c.id && (selected as string[]).includes(c.id))
.map(c => c.summary)
.join(', ')}
</Typography>
)}
MenuProps={{
PaperProps: {
style: {
width: 350,
},
},
}}
>
{sortBy(calendars, 'summary').map(c => (
<MenuItem key={c.id} value={c.id}>
<Checkbox checked={selectedCalendars.includes(c.id!)} />
<ListItemText primary={c.summary} />
</MenuItem>
))}
</Select>
</FormControl>
);
};
@@ -0,0 +1,62 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Box, Button, styled } from '@material-ui/core';
import { CalendarEvent } from './CalendarEvent';
import { events } from './signInEventMock';
type SignInContentProps = {
handleAuthClick: React.MouseEventHandler<HTMLElement>;
};
const TransparentBox = styled(Box)({
opacity: 0.3,
filter: 'blur(1.5px)',
});
export const SignInContent = ({ handleAuthClick }: SignInContentProps) => {
return (
<Box position="relative" height="100%" width="100%">
<TransparentBox p={1}>
{events.map(event => (
<CalendarEvent key={event.id} event={event} />
))}
</TransparentBox>
<Box
height="100%"
width="100%"
display="flex"
justifyContent="center"
alignItems="center"
position="absolute"
left={0}
top={0}
>
<Button
variant="contained"
color="primary"
onClick={handleAuthClick}
size="large"
>
Sign in
</Button>
</Box>
</Box>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { CalendarCardContainer } from './CalendarCardContainer';
@@ -0,0 +1,117 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GCalendarEvent } from './types';
export const events: GCalendarEvent[] = [
{
id: '1',
htmlLink: 'https://www.google.com/calendar/',
summary: 'Backstage Community Sessions',
start: {
dateTime: '2021-12-07T09:00:00+01:00',
timeZone: 'Europe/London',
},
end: {
dateTime: '2021-12-07T10:00:00+01:00',
timeZone: 'Europe/London',
},
},
{
id: '2',
htmlLink: 'https://www.google.com/calendar/',
summary: 'Backstage Community Sessions',
start: {
dateTime: '2021-12-07T10:30:00+01:00',
timeZone: 'Europe/London',
},
end: {
dateTime: '2021-12-07T10:45:00+01:00',
timeZone: 'Europe/London',
},
conferenceData: {
entryPoints: [
{
entryPointType: 'video',
uri: 'https://zoom.us',
},
],
},
},
{
id: '3',
htmlLink: 'https://www.google.com/calendar',
summary: 'Backstage Community Sessions',
start: {
dateTime: '2021-12-07T12:00:00+01:00',
timeZone: 'Europe/London',
},
end: {
dateTime: '2021-12-07T13:00:00+01:00',
timeZone: 'Europe/London',
},
conferenceData: {
entryPoints: [
{
entryPointType: 'video',
uri: 'https://zoom.us',
label: 'zoom.us',
},
],
},
},
{
id: '4',
htmlLink: 'https://www.google.com/calendar',
summary: 'Backstage Community Sessions',
start: {
dateTime: '2021-12-07T15:00:00+01:00',
timeZone: 'Europe/London',
},
end: {
dateTime: '2021-12-07T16:30:00+01:00',
timeZone: 'Europe/London',
},
conferenceData: {
entryPoints: [
{
entryPointType: 'video',
uri: 'https://zoom.us',
},
],
},
},
{
id: '5',
htmlLink: 'https://www.google.com/calendar',
summary: 'Backstage Community Sessions',
start: {
dateTime: '2021-12-07T17:00:00+01:00',
timeZone: 'Europe/London',
},
end: {
dateTime: '2021-12-07T17:30:00+01:00',
timeZone: 'Europe/London',
},
conferenceData: {
entryPoints: [
{
entryPointType: 'video',
uri: 'https://zoom.us',
},
],
},
},
];
@@ -0,0 +1,35 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types="gapi.auth2" />
/// <reference types="gapi.client.calendar" />
export type GCalendar = gapi.client.calendar.CalendarListEntry;
export type EventAttendee = gapi.client.calendar.EventAttendee;
export type GCalendarEvent = gapi.client.calendar.Event &
Pick<GCalendar, 'backgroundColor' | 'primary'> &
Pick<EventAttendee, 'responseStatus'> & {
calendarId?: string;
};
export enum ResponseStatus {
needsAction = 'needsAction',
accepted = 'accepted',
declined = 'declined',
maybe = 'tentative',
}
@@ -0,0 +1,82 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DateTime } from 'luxon';
import { GCalendarEvent } from './types';
export function getZoomLink(event: GCalendarEvent) {
const videoEntrypoint = event.conferenceData?.entryPoints?.find(
e => e.entryPointType === 'video',
);
return videoEntrypoint?.uri ?? '';
}
export function getTimePeriod(event: GCalendarEvent) {
if (isAllDay(event)) {
return getAllDayTimePeriod(event);
}
const format: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
};
const startTime = DateTime.fromISO(event.start?.dateTime || '');
const endTime = DateTime.fromISO(event.end?.dateTime || '');
return `${startTime.toLocaleString(format)} - ${endTime.toLocaleString(
format,
)}`;
}
function getAllDayTimePeriod(event: GCalendarEvent) {
const format: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric' };
const startTime = DateTime.fromISO(
event.start?.dateTime || event.start?.date || '',
);
const endTime = DateTime.fromISO(
event.end?.dateTime || event.end?.date || '',
).minus({ day: 1 });
if (startTime.toISO() === endTime.toISO()) {
return startTime.toLocaleString(format);
}
return `${startTime.toLocaleString(format)} - ${endTime.toLocaleString(
format,
)}`;
}
export function isPassed(event: GCalendarEvent) {
if (!event.end?.dateTime && !event.end?.date) return false;
const eventDate = DateTime.fromISO(event.end?.dateTime || event.end?.date!);
return DateTime.now() >= eventDate;
}
export function isAllDay(event: GCalendarEvent) {
if (event.start?.date || event.end?.date) {
return true;
}
const startTime = DateTime.fromISO(event.start?.dateTime || '');
const endTime = DateTime.fromISO(event.end?.dateTime || '');
return endTime.diff(startTime, 'day').days >= 1;
}
export function getStartDate(event: GCalendarEvent) {
return DateTime.fromISO(event.start?.dateTime || event.start?.date || '');
}
@@ -0,0 +1,19 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { useCalendarsQuery } from './useCalendarsQuery';
export { useEventsQuery } from './useEventsQuery';
export { useSignIn } from './useSignIn';
export { useStoredCalendars } from './useStoredCalendars';
@@ -0,0 +1,49 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useQuery } from 'react-query';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { gcalendarApiRef } from '../api';
type Options = {
enabled: boolean;
refreshTime?: number;
};
export const useCalendarsQuery = ({ enabled }: Options) => {
const calendarApi = useApi(gcalendarApiRef);
const errorApi = useApi(errorApiRef);
return useQuery(
['calendars'],
async () =>
calendarApi.getCalendars({
minAccessRole: 'reader',
}),
{
enabled,
keepPreviousData: true,
refetchInterval: 3600000,
onError: () => {
errorApi.post({
name: 'API error',
message: 'Failed to fetch calendars.',
});
},
},
);
};
@@ -0,0 +1,96 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { compact, unescape } from 'lodash';
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { useApi } from '@backstage/core-plugin-api';
import { gcalendarApiRef } from '../api';
import { GCalendar, GCalendarEvent } from '../components/CalendarCard/types';
type Options = {
selectedCalendars?: string[];
timeMin: string;
timeMax: string;
enabled: boolean;
calendars: GCalendar[];
timeZone: string;
refreshTime?: number;
};
export const useEventsQuery = ({
selectedCalendars = [],
calendars = [],
enabled,
timeMin,
timeMax,
timeZone,
}: Options) => {
const calendarApi = useApi(gcalendarApiRef);
const eventQueries = useQueries(
selectedCalendars
.filter(id => calendars.find(c => c.id === id))
.map(calendarId => {
const calendar = calendars.find(c => c.id === calendarId);
return {
queryKey: ['calendarEvents', calendarId, timeMin, timeMax],
enabled,
initialData: [],
refetchInterval: 60000,
refetchIntervalInBackground: true,
queryFn: async (): Promise<GCalendarEvent[]> => {
const data = await calendarApi.getEvents(calendarId, {
calendarId,
timeMin,
timeMax,
showDeleted: false,
singleEvents: true,
maxResults: 100,
orderBy: 'startTime',
timeZone,
});
return (data.items || []).map(event => {
const responseStatus = event.attendees?.find(
a => !!a.self,
)?.responseStatus;
return {
...event,
summary: unescape(event.summary || ''),
calendarId,
backgroundColor: calendar?.backgroundColor,
primary: !!calendar?.primary,
responseStatus,
};
});
},
};
}),
);
const events = useMemo(
() => compact(eventQueries.map(({ data }) => data).flat()),
[eventQueries],
);
const isLoading =
!!eventQueries.find(q => q.isFetching) && events.length === 0;
return { events, isLoading };
};
@@ -0,0 +1,42 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useState } from 'react';
import { googleAuthApiRef, useApi } from '@backstage/core-plugin-api';
export const useSignIn = () => {
const [isSignedIn, setSignedIn] = useState(false);
const [isInitialized, setInitialized] = useState(false);
const authApi = useApi(googleAuthApiRef);
const signIn = useCallback(
async (optional = false) => {
const token = await authApi.getAccessToken(
'https://www.googleapis.com/auth/calendar.readonly',
{
optional,
instantPopup: !optional,
},
);
setSignedIn(!!token);
setInitialized(true);
},
[authApi, setSignedIn],
);
return { isSignedIn, isInitialized, signIn };
};
@@ -0,0 +1,45 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useApi, storageApiRef } from '@backstage/core-plugin-api';
import { useObservable } from 'react-use';
import { gcalendarHomepagePlugin } from '../plugin';
export enum LocalStorageKeys {
selectedCalendars = 'google_calendars_selected',
}
export function useStoredCalendars(
defaultValue: string[],
): [string[], (value: string[]) => void] {
const storageBucket = gcalendarHomepagePlugin.getId();
const storageKey = LocalStorageKeys.selectedCalendars;
const storageApi = useApi(storageApiRef).forBucket(storageBucket);
const setValue = (value: string[]) => {
storageApi.set(storageKey, value);
};
const snapshot = useObservable(
storageApi.observe$<string[]>(storageKey),
storageApi.snapshot(storageKey),
);
let result: string[];
if (snapshot.presence === 'absent') {
result = defaultValue;
} else {
result = snapshot.value!;
}
return [result, setValue];
}
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="186 38 76 76">
<path fill="#fff" d="M244 56h-40v40h40V56z" />
<path fill="#EA4335" d="M244 114l18-18h-18v18z" />
<path fill="#FBBC04" d="M262 56h-18v40h18V56z" />
<path fill="#34A853" d="M244 96h-40v18h40V96z" />
<path fill="#188038" d="M186 96v12c0 3.315 2.685 6 6 6h12V96h-18z" />
<path fill="#1967D2" d="M262 56V44c0-3.315-2.685-6-6-6h-12v18h18z" />
<path fill="#4285F4" d="M244 38h-52c-3.315 0 -6 2.685-6 6v52h18V56h40V38z" />
<path fill="#4285F4" d="M212.205 87.03c-1.495-1.01-2.53-2.485-3.095-4.435l3.47-1.43c.315 1.2.865 2.13 1.65 2.79.78.66 1.73.985 2.84.985 1.135 0 2.11-.345 2.925-1.035s1.225-1.57 1.225-2.635c0-1.09-.43-1.98-1.29-2.67-.86-.69-1.94-1.035-3.23-1.035h-2.005V74.13h1.8c1.11 0 2.045-.3 2.805-.9.76-.6 1.14-1.42 1.14-2.465 0 -.93-.34-1.67-1.02-2.225-.68-.555-1.54-.835-2.585-.835-1.02 0 -1.83.27-2.43.815a4.784 4.784 0 0 0 -1.31 2.005l-3.435-1.43c.455-1.29 1.29-2.43 2.515-3.415 1.225-.985 2.79-1.48 4.69-1.48 1.405 0 2.67.27 3.79.815 1.12.545 2 1.3 2.635 2.26.635.965.95 2.045.95 3.245 0 1.225-.295 2.26-.885 3.11-.59.85-1.315 1.5-2.175 1.955v.205a6.605 6.605 0 0 1 2.79 2.175c.725.975 1.09 2.14 1.09 3.5 0 1.36-.345 2.575-1.035 3.64s-1.645 1.905-2.855 2.515c-1.215.61-2.58.92-4.095.92-1.755.005-3.375-.5-4.87-1.51zM233.52 69.81l-3.81 2.755-1.905-2.89 6.835-4.93h2.62V88h-3.74V69.81z" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="20" fill="#2196f3" />
<path fill="#fff" d="M29,31H14c-1.657,0-3-1.343-3-3V17h15c1.657,0,3,1.343,3,3V31z" />
<polygon fill="#fff" points="37,31 31,27 31,21 37,17" />
</svg>

After

Width:  |  Height:  |  Size: 265 B

+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { gcalendarHomepagePlugin, CalendarCard } from './plugin';
export * from './api';
@@ -0,0 +1,22 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { gcalendarHomepagePlugin } from './plugin';
describe('gcalendar-homepage', () => {
it('should export plugin', () => {
expect(gcalendarHomepagePlugin).toBeDefined();
});
});
+50
View File
@@ -0,0 +1,50 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createApiFactory,
createComponentExtension,
createPlugin,
googleAuthApiRef,
} from '@backstage/core-plugin-api';
import { GCalendarApiClient, gcalendarApiRef } from './api';
import { rootRouteRef } from './routes';
export const gcalendarHomepagePlugin = createPlugin({
id: 'gcalendar-homepage',
routes: {
root: rootRouteRef,
},
apis: [
createApiFactory({
api: gcalendarApiRef,
deps: { authApi: googleAuthApiRef },
factory(deps) {
return new GCalendarApiClient(deps);
},
}),
],
});
export const CalendarCard = gcalendarHomepagePlugin.provide(
createComponentExtension({
name: 'CalendarCard',
component: {
lazy: () =>
import('./components/CalendarCard').then(m => m.CalendarCardContainer),
},
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'gcalendar-homepage',
});
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';
+412 -6
View File
@@ -1302,6 +1302,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.17.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2":
version "7.17.2"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.16.7", "@babel/template@^7.3.3":
version "7.16.7"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
@@ -1793,11 +1800,49 @@
ms "^2.1.3"
secure-json-parse "^2.4.0"
"@emotion/cache@^11.7.1":
version "11.7.1"
resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539"
integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==
dependencies:
"@emotion/memoize" "^0.7.4"
"@emotion/sheet" "^1.1.0"
"@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5"
stylis "4.0.13"
"@emotion/hash@^0.8.0":
version "0.8.0"
resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
"@emotion/is-prop-valid@^1.1.1":
version "1.1.2"
resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.2.tgz#34ad6e98e871aa6f7a20469b602911b8b11b3a95"
integrity sha512-3QnhqeL+WW88YjYbQL5gUIkthuMw7a0NGbZ7wfFVk2kg/CK5w8w5FFa0RzWjyY1+sujN0NWbtSHH6OJmWHtJpQ==
dependencies:
"@emotion/memoize" "^0.7.4"
"@emotion/memoize@^0.7.4":
version "0.7.5"
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50"
integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
"@emotion/sheet@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2"
integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==
"@emotion/utils@^1.0.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf"
integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==
"@emotion/weak-memoize@^0.2.5":
version "0.2.5"
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
"@endemolshinegroup/cosmiconfig-typescript-loader@3.0.2":
version "3.0.2"
resolved "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz#eea4635828dde372838b0909693ebd9aafeec22d"
@@ -4004,6 +4049,13 @@
prop-types "^15.7.2"
react-is "^16.8.0 || ^17.0.0"
"@maxim_mazurok/gapi.client.calendar@latest":
version "3.0.20220211"
resolved "https://registry.npmjs.org/@maxim_mazurok/gapi.client.calendar/-/gapi.client.calendar-3.0.20220211.tgz#20125706c761c75219de15788cee100b7933fec6"
integrity sha512-ehK4pF4pJXjKFEXp+uiteq1hLAcf28ZbCxgzc6jL9XpNFDSpbhY2iPGU4lkWLS3ae4qrlScFCnmExem5Hg1gZQ==
dependencies:
"@types/gapi.client" "*"
"@microsoft/api-documenter@^7.15.0":
version "7.15.0"
resolved "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.15.0.tgz#e6cf24fc0e2f18a71dcf4c5c8100cc083167a81e"
@@ -4089,6 +4141,115 @@
outvariant "^1.2.0"
strict-event-emitter "^0.2.0"
"@mui/base@5.0.0-alpha.69":
version "5.0.0-alpha.69"
resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.69.tgz#8511198d760de0795870f5ec63e53db73ba801ec"
integrity sha512-IxUUj/lkilCTNBIybQxyQGW/zpxFp490G0QBQJgRp9TJkW2PWSTLvAH7gcH0YHd0L2TAf1TRgfdemoRseMzqQA==
dependencies:
"@babel/runtime" "^7.17.0"
"@emotion/is-prop-valid" "^1.1.1"
"@mui/utils" "^5.4.2"
"@popperjs/core" "^2.4.4"
clsx "^1.1.1"
prop-types "^15.7.2"
react-is "^17.0.2"
"@mui/icons-material@^5.0.0":
version "5.4.2"
resolved "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.4.2.tgz#b2fd2c6c81d2d275e17ce40bd50c63cb197d324b"
integrity sha512-7c+G3jBT+e+pN0a9DJ0Bd8Kr1Vy6os5Q1yd2aXcwuhlRI3uzJBLJ8sX6FSWoh5DSEBchb7Bsk1uHz6U0YN9l+Q==
dependencies:
"@babel/runtime" "^7.17.0"
"@mui/material@^5.0.0":
version "5.4.2"
resolved "https://registry.npmjs.org/@mui/material/-/material-5.4.2.tgz#04ea6632d7ca600a2ae528f6f140ef0af9c01434"
integrity sha512-jmeLWEO6AA6g7HErhI3MXVGaMZtqDZjDwcHCg24WY954wO38Xn0zJ53VfpFc44ZTJLV9Ejd7ci9fLlG/HmJCeg==
dependencies:
"@babel/runtime" "^7.17.0"
"@mui/base" "5.0.0-alpha.69"
"@mui/system" "^5.4.2"
"@mui/types" "^7.1.2"
"@mui/utils" "^5.4.2"
"@types/react-transition-group" "^4.4.4"
clsx "^1.1.1"
csstype "^3.0.10"
hoist-non-react-statics "^3.3.2"
prop-types "^15.7.2"
react-is "^17.0.2"
react-transition-group "^4.4.2"
"@mui/private-theming@^5.4.2":
version "5.4.2"
resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.4.2.tgz#f0a05f908456a2f7b87ccb6fc3b6e1faae9d89e6"
integrity sha512-mlPDYYko4wIcwXjCPEmOWbNTT4DZ6h9YHdnRtQPnWM28+TRUHEo7SbydnnmVDQLRXUfaH4Y6XtEHIfBNPE/SLg==
dependencies:
"@babel/runtime" "^7.17.0"
"@mui/utils" "^5.4.2"
prop-types "^15.7.2"
"@mui/styled-engine@^5.4.2":
version "5.4.2"
resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.4.2.tgz#e04903e06bd49fd10072a44ff38e13f5481bb64d"
integrity sha512-tz9p3aRtzXHKAg7x3BgP0hVQEoGKaxNCFxsJ+d/iqEHYvywWFSs6oxqYAvDHIRpvMlUZyPNoTrkcNnbdMmH/ng==
dependencies:
"@babel/runtime" "^7.17.0"
"@emotion/cache" "^11.7.1"
prop-types "^15.7.2"
"@mui/styles@^5.0.0":
version "5.4.2"
resolved "https://registry.npmjs.org/@mui/styles/-/styles-5.4.2.tgz#e0dadfc5de8255605f23c2f909f3669f0911bb88"
integrity sha512-BX75fNHmRF51yove9dBkH28gpSFjClOPDEnUwLTghPYN913OsqViS/iuCd61dxzygtEEmmeYuWfQjxu/F6vF5g==
dependencies:
"@babel/runtime" "^7.17.0"
"@emotion/hash" "^0.8.0"
"@mui/private-theming" "^5.4.2"
"@mui/types" "^7.1.2"
"@mui/utils" "^5.4.2"
clsx "^1.1.1"
csstype "^3.0.10"
hoist-non-react-statics "^3.3.2"
jss "^10.8.2"
jss-plugin-camel-case "^10.8.2"
jss-plugin-default-unit "^10.8.2"
jss-plugin-global "^10.8.2"
jss-plugin-nested "^10.8.2"
jss-plugin-props-sort "^10.8.2"
jss-plugin-rule-value-function "^10.8.2"
jss-plugin-vendor-prefixer "^10.8.2"
prop-types "^15.7.2"
"@mui/system@^5.4.2":
version "5.4.2"
resolved "https://registry.npmjs.org/@mui/system/-/system-5.4.2.tgz#8166e406ba4628950bd79cec8159de25d5aef162"
integrity sha512-QegBVu6fxUNov1X9bWc1MZUTeV3A5g9PIpli7d0kzkGfq6JzrJWuPlhSPZ+6hlWmWky+bbAXhU65Qz8atWxDGw==
dependencies:
"@babel/runtime" "^7.17.0"
"@mui/private-theming" "^5.4.2"
"@mui/styled-engine" "^5.4.2"
"@mui/types" "^7.1.2"
"@mui/utils" "^5.4.2"
clsx "^1.1.1"
csstype "^3.0.10"
prop-types "^15.7.2"
"@mui/types@^7.1.2":
version "7.1.2"
resolved "https://registry.npmjs.org/@mui/types/-/types-7.1.2.tgz#4f3678ae77a7a3efab73b6e040469cc6df2144ac"
integrity sha512-SD7O1nVzqG+ckQpFjDhXPZjRceB8HQFHEvdLLrPhlJy4lLbwEBbxK74Tj4t6Jgk0fTvLJisuwOutrtYe9P/xBQ==
"@mui/utils@^5.4.2":
version "5.4.2"
resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.4.2.tgz#3edda8f80de235418fff0424ee66e2a49793ec01"
integrity sha512-646dBCC57MXTo/Gf3AnZSHRHznaTETQq5x7AWp5FRQ4jPeyT4WSs18cpJVwkV01cAHKh06pNQTIufIALIWCL5g==
dependencies:
"@babel/runtime" "^7.17.0"
"@types/prop-types" "^15.7.4"
"@types/react-is" "^16.7.1 || ^17.0.0"
prop-types "^15.7.2"
react-is "^17.0.2"
"@n1ru4l/graphql-live-query@0.9.0", "@n1ru4l/graphql-live-query@^0.9.0":
version "0.9.0"
resolved "https://registry.npmjs.org/@n1ru4l/graphql-live-query/-/graphql-live-query-0.9.0.tgz#defaebdd31f625bee49e6745934f36312532b2bc"
@@ -4677,6 +4838,11 @@
resolved "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6"
integrity sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==
"@popperjs/core@^2.4.4":
version "2.11.2"
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9"
integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
@@ -5737,6 +5903,30 @@
dependencies:
"@types/node" "*"
"@types/gapi.auth2@^0.0.56":
version "0.0.56"
resolved "https://registry.npmjs.org/@types/gapi.auth2/-/gapi.auth2-0.0.56.tgz#2f7031f79390b8401e7950d8277ada874fd2731c"
integrity sha512-kGaBtGVCqGS3Y05L56dGVlBpJflxLfwA0zpMQnQgGRFk1tsMPbQnogG51UQjt1vCuYfRO0Jd9/K5KDtzjAbMkA==
dependencies:
"@types/gapi" "*"
"@types/gapi.client.calendar@^3.0.10":
version "3.0.10"
resolved "https://registry.npmjs.org/@types/gapi.client.calendar/-/gapi.client.calendar-3.0.10.tgz#4b089d9af2753a07cf1d46adc83b1a7cedb8e355"
integrity sha512-NUStEVbHPOhFsw4cWE2CThe5eKpTlmz+fSu8mvEc7j+IDVNgk1kS4C6hZzBCdlIjFfOzdQM3Cyqkt5kt7ze3kA==
dependencies:
"@maxim_mazurok/gapi.client.calendar" latest
"@types/gapi.client@*":
version "1.0.5"
resolved "https://registry.npmjs.org/@types/gapi.client/-/gapi.client-1.0.5.tgz#a6eb97e664fe51656c5b52258bd0afef28c76308"
integrity sha512-OTpbBMuzfC4lkvaomxqskI/iWRGW3zOZbDXZLNSyiuswTiSSGgILRLkg0POuZ4EgzEdaYaTlXpnXiCp07ri/Yw==
"@types/gapi@*", "@types/gapi@^0.0.41":
version "0.0.41"
resolved "https://registry.npmjs.org/@types/gapi/-/gapi-0.0.41.tgz#c477ee4f0951c005869219fd10b456ae2bba437e"
integrity sha512-tmHO66z/f91JZCDqinj/nNvQEszsz/hBT4+MvCSKT5sDzl5Ld/oXZ8WaecCBjRLw2uWKUInUHM9MhEXWkOiNjw==
"@types/git-url-parse@^9.0.0":
version "9.0.1"
resolved "https://registry.npmjs.org/@types/git-url-parse/-/git-url-parse-9.0.1.tgz#1c7cc89527ca8b5afcf260ead3b0e4e373c43938"
@@ -6189,7 +6379,7 @@
resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.0.0.tgz#dc85454b953178cc6043df5208b9e949b54a3bc4"
integrity sha512-/rM+sWiuOZ5dvuVzV37sUuklsbg+JPOP8d+nNFlo2ZtfpzPiPvh1/gc8liWOLBqe+sR+ZM7guPaIcTt6UZTo7Q==
"@types/prop-types@*", "@types/prop-types@^15.7.3":
"@types/prop-types@*", "@types/prop-types@^15.7.3", "@types/prop-types@^15.7.4":
version "15.7.4"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
@@ -6225,6 +6415,13 @@
dependencies:
"@types/react" "*"
"@types/react-is@^16.7.1 || ^17.0.0":
version "17.0.3"
resolved "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.16":
version "7.1.19"
resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.19.tgz#477bd0a9b01bae6d6bf809418cdfa7d3c16d4c62"
@@ -6270,6 +6467,13 @@
dependencies:
"@types/react" "*"
"@types/react-transition-group@^4.4.4":
version "4.4.4"
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e"
integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==
dependencies:
"@types/react" "*"
"@types/react-virtualized-auto-sizer@^1.0.1":
version "1.0.1"
resolved "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4"
@@ -6362,6 +6566,13 @@
dependencies:
rollup-plugin-postcss "*"
"@types/sanitize-html@^2.6.2":
version "2.6.2"
resolved "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.6.2.tgz#9c47960841b9def1e4c9dfebaaab010a3f6e97b9"
integrity sha512-7Lu2zMQnmHHQGKXVvCOhSziQMpa+R2hMHFefzbYoYMHeaXR0uXqNeOc3JeQQQ8/6Xa2Br/P1IQTLzV09xxAiUQ==
dependencies:
htmlparser2 "^6.0.0"
"@types/scheduler@*":
version "0.16.1"
resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
@@ -7760,6 +7971,13 @@ axios@^0.21.1, axios@^0.21.4:
dependencies:
follow-redirects "^1.14.0"
axios@^0.26.0:
version "0.26.0"
resolved "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz#9a318f1c69ec108f8cd5f3c3d390366635e13928"
integrity sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==
dependencies:
follow-redirects "^1.14.8"
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -8044,6 +8262,11 @@ bfj@^7.0.2:
hoopy "^0.1.4"
tryer "^1.0.1"
big-integer@^1.6.16:
version "1.6.51"
resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686"
integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==
big-integer@^1.6.17:
version "1.6.48"
resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
@@ -8235,6 +8458,20 @@ breakword@^1.0.5:
dependencies:
wcwidth "^1.0.1"
broadcast-channel@^3.4.1:
version "3.7.0"
resolved "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937"
integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==
dependencies:
"@babel/runtime" "^7.7.2"
detect-node "^2.1.0"
js-sha3 "0.8.0"
microseconds "0.2.0"
nano-time "1.0.0"
oblivious-set "1.0.0"
rimraf "3.0.2"
unload "2.2.0"
brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -9009,7 +9246,7 @@ cloneable-readable@^1.0.0:
process-nextick-args "^2.0.0"
readable-stream "^2.3.5"
clsx@^1.0.2, clsx@^1.0.4:
clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
@@ -10008,6 +10245,11 @@ csstype@^2.5.2, csstype@^2.6.7:
resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz#4cf30eb87e1d1a005d8b6510f95292413f6a1c0e"
integrity sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==
csstype@^3.0.10:
version "3.0.10"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
csstype@^3.0.2, csstype@^3.0.6:
version "3.0.7"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b"
@@ -10711,6 +10953,11 @@ detect-node@^2.0.4:
resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
detect-node@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
detect-port-alt@^1.1.6:
version "1.1.6"
resolved "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275"
@@ -12588,6 +12835,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.0:
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
follow-redirects@^1.14.8:
version "1.14.9"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -13800,7 +14052,7 @@ html-webpack-plugin@^5.3.1:
pretty-error "^4.0.0"
tapable "^2.0.0"
htmlparser2@^6.1.0:
htmlparser2@^6.0.0, htmlparser2@^6.1.0:
version "6.1.0"
resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
@@ -15588,6 +15840,11 @@ js-levenshtein@^1.1.6:
resolved "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
js-sha3@0.8.0:
version "0.8.0"
resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -15972,6 +16229,15 @@ jss-plugin-camel-case@^10.5.1:
hyphenate-style-name "^1.0.3"
jss "10.6.0"
jss-plugin-camel-case@^10.8.2:
version "10.9.0"
resolved "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz#4921b568b38d893f39736ee8c4c5f1c64670aaf7"
integrity sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww==
dependencies:
"@babel/runtime" "^7.3.1"
hyphenate-style-name "^1.0.3"
jss "10.9.0"
jss-plugin-default-unit@^10.5.1:
version "10.6.0"
resolved "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.6.0.tgz#af47972486819b375f0f3a9e0213403a84b5ef3b"
@@ -15980,6 +16246,14 @@ jss-plugin-default-unit@^10.5.1:
"@babel/runtime" "^7.3.1"
jss "10.6.0"
jss-plugin-default-unit@^10.8.2:
version "10.9.0"
resolved "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz#bb23a48f075bc0ce852b4b4d3f7582bc002df991"
integrity sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
jss-plugin-global@^10.5.1:
version "10.6.0"
resolved "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.6.0.tgz#3e8011f760f399cbadcca7f10a485b729c50e3ed"
@@ -15988,6 +16262,14 @@ jss-plugin-global@^10.5.1:
"@babel/runtime" "^7.3.1"
jss "10.6.0"
jss-plugin-global@^10.8.2:
version "10.9.0"
resolved "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz#fc07a0086ac97aca174e37edb480b69277f3931f"
integrity sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
jss-plugin-nested@^10.5.1:
version "10.6.0"
resolved "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.6.0.tgz#5f83c5c337d3b38004834e8426957715a0251641"
@@ -15997,6 +16279,15 @@ jss-plugin-nested@^10.5.1:
jss "10.6.0"
tiny-warning "^1.0.2"
jss-plugin-nested@^10.8.2:
version "10.9.0"
resolved "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz#cc1c7d63ad542c3ccc6e2c66c8328c6b6b00f4b3"
integrity sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
tiny-warning "^1.0.2"
jss-plugin-props-sort@^10.5.1:
version "10.6.0"
resolved "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.6.0.tgz#297879f35f9fe21196448579fee37bcde28ce6bc"
@@ -16005,6 +16296,14 @@ jss-plugin-props-sort@^10.5.1:
"@babel/runtime" "^7.3.1"
jss "10.6.0"
jss-plugin-props-sort@^10.8.2:
version "10.9.0"
resolved "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz#30e9567ef9479043feb6e5e59db09b4de687c47d"
integrity sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
jss-plugin-rule-value-function@^10.5.1:
version "10.6.0"
resolved "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.6.0.tgz#3c1a557236a139d0151e70a82c810ccce1c1c5ea"
@@ -16014,6 +16313,15 @@ jss-plugin-rule-value-function@^10.5.1:
jss "10.6.0"
tiny-warning "^1.0.2"
jss-plugin-rule-value-function@^10.8.2:
version "10.9.0"
resolved "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz#379fd2732c0746fe45168011fe25544c1a295d67"
integrity sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
tiny-warning "^1.0.2"
jss-plugin-vendor-prefixer@^10.5.1:
version "10.6.0"
resolved "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.6.0.tgz#e1fcd499352846890c38085b11dbd7aa1c4f2c78"
@@ -16023,6 +16331,15 @@ jss-plugin-vendor-prefixer@^10.5.1:
css-vendor "^2.0.8"
jss "10.6.0"
jss-plugin-vendor-prefixer@^10.8.2:
version "10.9.0"
resolved "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz#aa9df98abfb3f75f7ed59a3ec50a5452461a206a"
integrity sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA==
dependencies:
"@babel/runtime" "^7.3.1"
css-vendor "^2.0.8"
jss "10.9.0"
jss@10.6.0, jss@^10.5.1:
version "10.6.0"
resolved "https://registry.npmjs.org/jss/-/jss-10.6.0.tgz#d92ff9d0f214f65ca1718591b68e107be4774149"
@@ -16034,6 +16351,16 @@ jss@10.6.0, jss@^10.5.1:
is-in-browser "^1.1.3"
tiny-warning "^1.0.2"
jss@10.9.0, jss@^10.8.2:
version "10.9.0"
resolved "https://registry.npmjs.org/jss/-/jss-10.9.0.tgz#7583ee2cdc904a83c872ba695d1baab4b59c141b"
integrity sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw==
dependencies:
"@babel/runtime" "^7.3.1"
csstype "^3.0.2"
is-in-browser "^1.1.3"
tiny-warning "^1.0.2"
"jsx-ast-utils@^2.4.1 || ^3.0.0":
version "3.2.0"
resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82"
@@ -16910,7 +17237,7 @@ lunr@^2.3.9:
resolved "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==
luxon@^2.0.2:
luxon@^2.0.2, luxon@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/luxon/-/luxon-2.3.0.tgz#bf16a7e642513c2a20a6230a6a41b0ab446d0045"
integrity sha512-gv6jZCV+gGIrVKhO90yrsn8qXPKD8HYZJtrUDSfEbow8Tkw84T9OnCyJhWvnJIaIF/tBuiAjZuQHUt1LddX2mg==
@@ -17079,6 +17406,26 @@ marked@^4.0.10:
resolved "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz#423e295385cc0c3a70fa495e0df68b007b879423"
integrity sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==
match-sorter@^6.0.2:
version "6.3.1"
resolved "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==
dependencies:
"@babel/runtime" "^7.12.5"
remove-accents "0.4.2"
material-ui-popup-state@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-2.0.0.tgz#8050421a9d00df67d7f63e7b2b73ab5b331051ff"
integrity sha512-1sbb9xpMs7OxG0SOGfGO0ZnwiLtqZSoXda0/AqqJkpouT4e0nADXutQtDJFMa9GUMNAODVDlYnNmfqM+MhFjsg==
dependencies:
"@babel/runtime" "^7.12.5"
"@mui/icons-material" "^5.0.0"
"@mui/material" "^5.0.0"
"@mui/styles" "^5.0.0"
classnames "^2.2.6"
prop-types "^15.7.2"
material-ui-search-bar@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/material-ui-search-bar/-/material-ui-search-bar-1.0.0.tgz#2652dd5bdc4cb043cffb7144d9c296c120702e62"
@@ -17684,6 +18031,11 @@ micromatch@^4.0.2, micromatch@^4.0.4:
braces "^3.0.1"
picomatch "^2.2.3"
microseconds@0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -18134,6 +18486,13 @@ nano-css@^5.3.1:
stacktrace-js "^2.0.2"
stylis "^4.0.6"
nano-time@1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=
dependencies:
big-integer "^1.6.16"
nanoclone@^0.2.1:
version "0.2.1"
resolved "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
@@ -18770,6 +19129,11 @@ object.values@^1.1.5:
define-properties "^1.1.3"
es-abstract "^1.19.1"
oblivious-set@1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566"
integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==
obuf@^1.0.0, obuf@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
@@ -19329,6 +19693,11 @@ parse-path@^4.0.0:
is-ssh "^1.3.0"
protocols "^1.4.0"
parse-srcset@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=
parse-url@^5.0.0:
version "5.0.1"
resolved "https://registry.npmjs.org/parse-url/-/parse-url-5.0.1.tgz#99c4084fc11be14141efa41b3d117a96fcb9527f"
@@ -20141,7 +20510,7 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.1.0, postcss@^8.4.5:
postcss@^8.1.0, postcss@^8.3.11, postcss@^8.4.5:
version "8.4.6"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1"
integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==
@@ -20842,7 +21211,7 @@ react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.0, react-is@^17.0.1:
"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
@@ -20872,6 +21241,15 @@ react-markdown@^8.0.0:
unist-util-visit "^4.0.0"
vfile "^5.0.0"
react-query@^3.34.16:
version "3.34.16"
resolved "https://registry.npmjs.org/react-query/-/react-query-3.34.16.tgz#279ea180bcaeaec49c7864b29d1711ee9f152594"
integrity sha512-7FvBvjgEM4YQ8nPfmAr+lJfbW95uyW/TVjFoi2GwCkF33/S8ajx45tuPHPFGWs4qYwPy1mzwxD4IQfpUDrefNQ==
dependencies:
"@babel/runtime" "^7.5.5"
broadcast-channel "^3.4.1"
match-sorter "^6.0.2"
react-redux@^7.1.1, react-redux@^7.2.4:
version "7.2.5"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.5.tgz#213c1b05aa1187d9c940ddfc0b29450957f6a3b8"
@@ -21004,6 +21382,16 @@ react-transition-group@^4.0.0, react-transition-group@^4.4.0:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-transition-group@^4.4.2:
version "4.4.2"
resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==
dependencies:
"@babel/runtime" "^7.5.5"
dom-helpers "^5.0.1"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-universal-interface@^0.6.2:
version "0.6.2"
resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"
@@ -21608,6 +21996,11 @@ remedial@^1.0.7:
resolved "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz#a5e4fd52a0e4956adbaf62da63a5a46a78c578a0"
integrity sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==
remove-accents@0.4.2:
version "0.4.2"
resolved "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
integrity sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -23354,6 +23747,11 @@ stylehacks@^5.0.1:
browserslist "^4.16.0"
postcss-selector-parser "^6.0.4"
stylis@4.0.13:
version "4.0.13"
resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
stylis@^4.0.6:
version "4.0.7"
resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.7.tgz#412a90c28079417f3d27c028035095e4232d2904"
@@ -24636,6 +25034,14 @@ unixify@1.0.0, unixify@^1.0.0:
dependencies:
normalize-path "^2.1.1"
unload@2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7"
integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==
dependencies:
"@babel/runtime" "^7.6.2"
detect-node "^2.0.4"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"