diff --git a/packages/app/package.json b/packages/app/package.json index 25dcbbd945..492ca8e900 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index d587a88402..c0d6b0cea5 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -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 }, diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx index 5a19cef6f6..4e83920121 100644 --- a/packages/app/src/components/home/HomePage.tsx +++ b/packages/app/src/components/home/HomePage.tsx @@ -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 = () => ( ]} /> + + + diff --git a/plugins/gcalendar-homepage/.eslintrc.js b/plugins/gcalendar-homepage/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/gcalendar-homepage/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/gcalendar-homepage/README.md b/plugins/gcalendar-homepage/README.md new file mode 100644 index 0000000000..6a7bfd9e33 --- /dev/null +++ b/plugins/gcalendar-homepage/README.md @@ -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. diff --git a/plugins/gcalendar-homepage/dev/index.tsx b/plugins/gcalendar-homepage/dev/index.tsx new file mode 100644 index 0000000000..1fd27d507d --- /dev/null +++ b/plugins/gcalendar-homepage/dev/index.tsx @@ -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(); diff --git a/plugins/gcalendar-homepage/package.json b/plugins/gcalendar-homepage/package.json new file mode 100644 index 0000000000..871c714823 --- /dev/null +++ b/plugins/gcalendar-homepage/package.json @@ -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" + ] +} diff --git a/plugins/gcalendar-homepage/src/api/index.ts b/plugins/gcalendar-homepage/src/api/index.ts new file mode 100644 index 0000000000..58fae449c3 --- /dev/null +++ b/plugins/gcalendar-homepage/src/api/index.ts @@ -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({ + 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; + } +} diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/AttendeeChip.test.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/AttendeeChip.test.tsx new file mode 100644 index 0000000000..da9c587f23 --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/AttendeeChip.test.tsx @@ -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('', () => { + it('renders attendee email', async () => { + const email = 'test@test.com'; + const user: EventAttendee = { + email, + responseStatus: ResponseStatus.needsAction, + }; + const { queryByText } = await renderInTestApp(); + 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(); + 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(); + expect(getByTestId('declined-icon')).toBeInTheDocument(); + }); +}); diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/AttendeeChip.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/AttendeeChip.tsx new file mode 100644 index 0000000000..d06e69aa49 --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/AttendeeChip.tsx @@ -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 ; + } + if (responseStatus === ResponseStatus.declined) { + return ; + } + + return null; +}; + +type AttendeeChipProps = { + user: EventAttendee; +}; + +export const AttendeeChip = ({ user }: AttendeeChipProps) => { + const classes = useStyles({ responseStatus: user.responseStatus }); + + return ( + } + > + + + ); +}; diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarCard.test.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarCard.test.tsx new file mode 100644 index 0000000000..1b53d329d5 --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarCard.test.tsx @@ -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('', () => { + 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( + + + , + ); + + expect(rendered.queryByText('Sign in')).toBeInTheDocument(); + }); + + it('should render empty card', async () => { + const rendered = await renderInTestApp( + + + , + ); + + 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( + + + , + ); + + expect(rendered.queryByText(primaryCalendar.summary)).toBeInTheDocument(); + expect( + rendered.queryByText(nonPrimaryCalendar.summary), + ).not.toBeInTheDocument(); + }); + + it('should render calendar events', async () => { + const rendered = await renderInTestApp( + + + , + ); + + 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( + + + , + ); + + expect( + rendered.queryByText(nonPrimaryCalendar.summary), + ).toBeInTheDocument(); + expect( + rendered.queryByText(primaryCalendar.summary), + ).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarCard.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarCard.tsx new file mode 100644 index 0000000000..8111e68f84 --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarCard.tsx @@ -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 ( + + + Google Calendar + + {isSignedIn ? ( + <> + changeDay(-1)} size="small"> + + + changeDay(1)} size="small"> + + + + + {date.toLocaleString({ + weekday: 'short', + month: 'short', + day: 'numeric', + })} + + + + + + + ) : ( + Agenda + )} + + } + deepLink={{ + link: 'https://calendar.google.com/', + title: 'Go to Calendar', + }} + > + + {(isCalendarLoading || !isInitialized || isEventLoading) && ( + + + + )} + {!isSignedIn && isInitialized && ( + signIn(false)} /> + )} + {!isEventLoading && !isCalendarLoading && isSignedIn && ( + + {events.length === 0 && ( + + + No events + + + )} + {sortBy(events, [getStartDate]).map(event => ( + + ))} + + )} + + + ); +}; diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarCardContainer.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarCardContainer.tsx new file mode 100644 index 0000000000..b1595308ad --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarCardContainer.tsx @@ -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 ( + + + + ); +}; diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEvent.test.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEvent.test.tsx new file mode 100644 index 0000000000..7a48903bec --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEvent.test.tsx @@ -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('', () => { + 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( + , + ); + 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( + , + ); + expect(queryByText(allDayEvent.summary)).toBeInTheDocument(); + expect(queryByTestId('calendar-event-time')).not.toBeInTheDocument(); + }); + + it('should show popover on click', async () => { + const { queryByTestId, getByTestId } = await renderInTestApp( + , + ); + expect(queryByTestId('calendar-event-popover')).not.toBeInTheDocument(); + + fireEvent.click(getByTestId('calendar-event')); + expect(queryByTestId('calendar-event-popover')).toBeInTheDocument(); + }); +}); diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEvent.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEvent.tsx new file mode 100644 index 0000000000..4d448c9cd2 --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEvent.tsx @@ -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 ( + <> + { + 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" + > + + + + {event.summary} + + {!isAllDay(event) && ( + + {getTimePeriod(event)} + + )} + + + {zoomLink && ( + + { + e.stopPropagation(); + analytics.captureEvent('click', 'zoom link'); + }} + > + Zoom link + + + )} + + + + + + + ); +}; diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEventPopoverContent.test.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEventPopoverContent.test.tsx new file mode 100644 index 0000000000..fe48b2fd5a --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEventPopoverContent.test.tsx @@ -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('', () => { + 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( + , + ); + 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(); + }); +}); diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEventPopoverContent.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEventPopoverContent.tsx new file mode 100644 index 0000000000..749c3cd8ba --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarEventPopoverContent.tsx @@ -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 ( + + + + {event.summary} + {getTimePeriod(event)} + + {event.htmlLink && ( + + + analytics.captureEvent('click', 'open in calendar') + } + > + + + + + + )} + + {zoomLink && ( + analytics.captureEvent('click', 'zoom link')} + > + Join Zoom Meeting + + )} + + {event.description && ( + <> + + + + )} + + {event.attendees && ( + <> + + + Attendees + + {sortBy(event.attendees || [], 'email').map(user => ( + + ))} + + + )} + + ); +}; diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarSelect.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarSelect.tsx new file mode 100644 index 0000000000..71d25329b5 --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/CalendarSelect.tsx @@ -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 ( + + } + renderValue={selected => ( + + {calendars + .filter(c => c.id && (selected as string[]).includes(c.id)) + .map(c => c.summary) + .join(', ')} + + )} + MenuProps={{ + PaperProps: { + style: { + width: 350, + }, + }, + }} + > + {sortBy(calendars, 'summary').map(c => ( + + + + + ))} + + + ); +}; diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/SignInContent.tsx b/plugins/gcalendar-homepage/src/components/CalendarCard/SignInContent.tsx new file mode 100644 index 0000000000..c748fecea0 --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/SignInContent.tsx @@ -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; +}; + +const TransparentBox = styled(Box)({ + opacity: 0.3, + filter: 'blur(1.5px)', +}); + +export const SignInContent = ({ handleAuthClick }: SignInContentProps) => { + return ( + + + {events.map(event => ( + + ))} + + + + + + + ); +}; diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/index.ts b/plugins/gcalendar-homepage/src/components/CalendarCard/index.ts new file mode 100644 index 0000000000..c6f80f89ee --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/index.ts @@ -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'; diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/signInEventMock.ts b/plugins/gcalendar-homepage/src/components/CalendarCard/signInEventMock.ts new file mode 100644 index 0000000000..8d76757a59 --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/signInEventMock.ts @@ -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', + }, + ], + }, + }, +]; diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/types.ts b/plugins/gcalendar-homepage/src/components/CalendarCard/types.ts new file mode 100644 index 0000000000..1423b91aca --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/types.ts @@ -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. + */ + +/// +/// + +export type GCalendar = gapi.client.calendar.CalendarListEntry; + +export type EventAttendee = gapi.client.calendar.EventAttendee; + +export type GCalendarEvent = gapi.client.calendar.Event & + Pick & + Pick & { + calendarId?: string; + }; + +export enum ResponseStatus { + needsAction = 'needsAction', + accepted = 'accepted', + declined = 'declined', + maybe = 'tentative', +} diff --git a/plugins/gcalendar-homepage/src/components/CalendarCard/util.ts b/plugins/gcalendar-homepage/src/components/CalendarCard/util.ts new file mode 100644 index 0000000000..e0c2abf259 --- /dev/null +++ b/plugins/gcalendar-homepage/src/components/CalendarCard/util.ts @@ -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 || ''); +} diff --git a/plugins/gcalendar-homepage/src/hooks/index.ts b/plugins/gcalendar-homepage/src/hooks/index.ts new file mode 100644 index 0000000000..ef922cc4a3 --- /dev/null +++ b/plugins/gcalendar-homepage/src/hooks/index.ts @@ -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'; diff --git a/plugins/gcalendar-homepage/src/hooks/useCalendarsQuery.ts b/plugins/gcalendar-homepage/src/hooks/useCalendarsQuery.ts new file mode 100644 index 0000000000..ed31e23c79 --- /dev/null +++ b/plugins/gcalendar-homepage/src/hooks/useCalendarsQuery.ts @@ -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.', + }); + }, + }, + ); +}; diff --git a/plugins/gcalendar-homepage/src/hooks/useEventsQuery.ts b/plugins/gcalendar-homepage/src/hooks/useEventsQuery.ts new file mode 100644 index 0000000000..7630c82b0d --- /dev/null +++ b/plugins/gcalendar-homepage/src/hooks/useEventsQuery.ts @@ -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 => { + 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 }; +}; diff --git a/plugins/gcalendar-homepage/src/hooks/useSignIn.ts b/plugins/gcalendar-homepage/src/hooks/useSignIn.ts new file mode 100644 index 0000000000..351707b54e --- /dev/null +++ b/plugins/gcalendar-homepage/src/hooks/useSignIn.ts @@ -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 }; +}; diff --git a/plugins/gcalendar-homepage/src/hooks/useStoredCalendars.ts b/plugins/gcalendar-homepage/src/hooks/useStoredCalendars.ts new file mode 100644 index 0000000000..8c6b8c3f82 --- /dev/null +++ b/plugins/gcalendar-homepage/src/hooks/useStoredCalendars.ts @@ -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$(storageKey), + storageApi.snapshot(storageKey), + ); + let result: string[]; + if (snapshot.presence === 'absent') { + result = defaultValue; + } else { + result = snapshot.value!; + } + return [result, setValue]; +} diff --git a/plugins/gcalendar-homepage/src/icons/calendarIcon.svg b/plugins/gcalendar-homepage/src/icons/calendarIcon.svg new file mode 100644 index 0000000000..f5b82c824c --- /dev/null +++ b/plugins/gcalendar-homepage/src/icons/calendarIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/plugins/gcalendar-homepage/src/icons/zoomIcon.svg b/plugins/gcalendar-homepage/src/icons/zoomIcon.svg new file mode 100644 index 0000000000..cc6ef0316b --- /dev/null +++ b/plugins/gcalendar-homepage/src/icons/zoomIcon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/plugins/gcalendar-homepage/src/index.ts b/plugins/gcalendar-homepage/src/index.ts new file mode 100644 index 0000000000..faaeabf754 --- /dev/null +++ b/plugins/gcalendar-homepage/src/index.ts @@ -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'; diff --git a/plugins/gcalendar-homepage/src/plugin.test.ts b/plugins/gcalendar-homepage/src/plugin.test.ts new file mode 100644 index 0000000000..e5604f39dc --- /dev/null +++ b/plugins/gcalendar-homepage/src/plugin.test.ts @@ -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(); + }); +}); diff --git a/plugins/gcalendar-homepage/src/plugin.ts b/plugins/gcalendar-homepage/src/plugin.ts new file mode 100644 index 0000000000..878475d1c8 --- /dev/null +++ b/plugins/gcalendar-homepage/src/plugin.ts @@ -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), + }, + }), +); diff --git a/plugins/gcalendar-homepage/src/routes.ts b/plugins/gcalendar-homepage/src/routes.ts new file mode 100644 index 0000000000..3831bb6e37 --- /dev/null +++ b/plugins/gcalendar-homepage/src/routes.ts @@ -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', +}); diff --git a/plugins/gcalendar-homepage/src/setupTests.ts b/plugins/gcalendar-homepage/src/setupTests.ts new file mode 100644 index 0000000000..9bb3e72355 --- /dev/null +++ b/plugins/gcalendar-homepage/src/setupTests.ts @@ -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'; diff --git a/yarn.lock b/yarn.lock index d432deb3b3..10257a38e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"