Merge pull request #16101 from Abhay-soni-developer/develop/microsoft-calendar

Microsoft Calendar Plugin
This commit is contained in:
Ben Lambert
2023-02-09 09:39:51 +01:00
committed by GitHub
35 changed files with 1991 additions and 1 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-microsoft-calendar': minor
---
Created a new plugin `@backstage/plugin-microsoft-calendar` to display events from a Microsoft Calendar.
please refer to the [README.md](https://github.com/backstage/backstage/blob/master/plugins/microsoft-calendar/README.md) for step-by-step instructions to setup the plugin in your Backstage instance.
+1
View File
@@ -59,6 +59,7 @@ yarn.lock @backstage/maintainers @back
/plugins/kafka-backend @backstage/maintainers @nirga @andrewthauer
/plugins/kubernetes @backstage/maintainers @backstage/warpspeed
/plugins/kubernetes-* @backstage/maintainers @backstage/warpspeed
/plugins/microsoft-calendar @backstage/maintainers @abhay-soni-developer
/plugins/newrelic-dashboard @backstage/maintainers @mufaddal7
/plugins/playlist @backstage/maintainers @kuangp
/plugins/playlist-* @backstage/maintainers @kuangp
+1
View File
@@ -41,6 +41,7 @@
"@backstage/plugin-kafka": "workspace:^",
"@backstage/plugin-kubernetes": "workspace:^",
"@backstage/plugin-lighthouse": "workspace:^",
"@backstage/plugin-microsoft-calendar": "workspace:^",
"@backstage/plugin-newrelic": "workspace:^",
"@backstage/plugin-newrelic-dashboard": "workspace:^",
"@backstage/plugin-org": "workspace:^",
@@ -27,6 +27,7 @@ import {
import { Content, Header, Page } from '@backstage/core-components';
import { HomePageSearchBar } from '@backstage/plugin-search';
import { HomePageCalendar } from '@backstage/plugin-gcalendar';
import { MicrosoftCalendarCard } from '@backstage/plugin-microsoft-calendar';
import Grid from '@material-ui/core/Grid';
import React from 'react';
@@ -114,6 +115,9 @@ export const homePage = (
<Grid item xs={12} md={4}>
<HomePageCalendar />
</Grid>
<Grid item xs={12} md={4}>
<MicrosoftCalendarCard />
</Grid>
<Grid item xs={12} md={4}>
<HomePageStarredEntities />
</Grid>
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+51
View File
@@ -0,0 +1,51 @@
# Microsoft-Calendar Plugin
Welcome to the Microsoft calendar plugin!
_This plugin was created through the Backstage CLI_
## Features
1. You can switch between calendars, using the select menu on the calendar card header.
2. Card showing the list of events on the selected date and the selected calendar (provided by Outlook calendar).
3. Link to join the online meeting on the event card if provided. so you can join your meetings right away hassle-free.
4. Hovering over the event will pop over a card showing the event summary message, and list of attendees.
5. attendee's chips will have a badge over them symbolizing their responses.
- green --> accepted
- red --> declined
- nothing --> not responded yet
## Setup
The following sections will help you set up the Microsoft calendar plugin.
### Microsoft azure authentication provider
> You need to setup [Microsoft Azure authentication provider](https://backstage.io/docs/auth/microsoft/provider), before you move forward with any of the below step if you haven't already.
1. Install the plugin by running this command
```bash
# From the Backstage repository root
yarn add --cwd packages/app @backstage/plugin-microsoft-calendar
```
2. Import the Microsoft calendar React component from `@backstage/plugin-microsoft-calendar`.
3. You can then use the provided React component `MicrosoftCalendar` in the backstage frontend where ever you want
```tsx
import { MicrosoftCalendar } from '@backstage/plugin-microsoft-calendar';
// ...
<Grid item xs={12} md={4}>
<MicrosoftCalendar />
</Grid>;
// ...
```
![Microsoft Calendar plugin demo](https://user-images.githubusercontent.com/23618736/215717491-25db5fa6-b237-487f-8c00-28f572e8da05.mp4)
![Sample](./docs/microsoft-calendar-plugin.png)
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.
+39
View File
@@ -0,0 +1,39 @@
## API Report File for "@backstage/plugin-microsoft-calendar"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { ApiRef } from '@backstage/core-plugin-api';
import { Calendar } from '@microsoft/microsoft-graph-types';
import { Event as Event_2 } from '@microsoft/microsoft-graph-types';
import { FetchApi } from '@backstage/core-plugin-api';
import { OAuthApi } from '@backstage/core-plugin-api';
// @public (undocumented)
export class MicrosoftCalendarApiClient {
constructor(options: { authApi: OAuthApi; fetchApi: FetchApi });
// (undocumented)
getCalendars(): Promise<Calendar[]>;
// (undocumented)
getEvents(
calendarId: string,
params: {
startDateTime: string;
endDateTime: string;
},
headers: {
[key in string]: any;
},
): Promise<Event_2[]>;
}
// @public (undocumented)
export const microsoftCalendarApiRef: ApiRef<MicrosoftCalendarApiClient>;
// @public (undocumented)
export const MicrosoftCalendarCard: () => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright 2023 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 { createDevApp } from '@backstage/dev-utils';
import { microsoftCalendarPlugin, MicrosoftCalendarCard } from '../src/plugin';
import { microsoftCalendarApiRef } from '../src';
import responseMock from './mock.json';
import { microsoftAuthApiRef } from '@backstage/core-plugin-api';
import { Content, Page } from '@backstage/core-components';
import { Grid } from '@material-ui/core';
createDevApp()
.registerPlugin(microsoftCalendarPlugin)
.registerApi({
api: microsoftAuthApiRef,
deps: {},
factory: () =>
({
async getAccessToken() {
return Promise.resolve('token');
},
} as unknown as typeof microsoftAuthApiRef.T),
})
.registerApi({
api: microsoftCalendarApiRef,
deps: {},
factory: () =>
({
async getCalendars() {
return Promise.resolve(responseMock.calendars);
},
async getEvents() {
return Promise.resolve(responseMock.events);
},
} as unknown as typeof microsoftCalendarApiRef.T),
})
.addPage({
element: (
<Page themeId="home">
<Content>
<Grid item xs={12} md={6}>
<MicrosoftCalendarCard />
</Grid>
</Content>
</Page>
),
title: 'Microsoft-Calendar Plugin Demo',
path: '/microsoft-calendar',
})
.render();
+305
View File
@@ -0,0 +1,305 @@
{
"calendars": [
{
"id": "AQMkADQyMDdlODU1AC05OGNiLTRhZmItOGM2Mi1kODZhMzJkYWU2OTAARgAAAwWXy9axQ31DtBuHDIFwgtYHAKRSO4MUJXZDi1hwzpYLetAAAAIBBgAAAKRSO4MUJXZDi1hwzpYLetAAAAIyswAAAA==",
"name": "Calendar",
"color": "lightBlue",
"hexColor": "#9fe1e7",
"isDefaultCalendar": true,
"changeKey": "pFI7gxQldkOLWHDOlgt60AAAAAADYw==",
"canShare": true,
"canViewPrivateItems": true,
"canEdit": true,
"allowedOnlineMeetingProviders": ["teamsForBusiness"],
"defaultOnlineMeetingProvider": "teamsForBusiness",
"isTallyingResponses": true,
"isRemovable": false,
"owner": {
"name": "Abhay Soni",
"address": "abhaysoni.developer@gmail.com"
}
},
{
"id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAEGAACkUjuDFCV2Q4tYcM6WC3rQAAAZs65BAAA=",
"name": "United Kingdom holidays",
"color": "auto",
"hexColor": "",
"isDefaultCalendar": false,
"changeKey": "pFI7gxQldkOLWHDOlgt60AAAGanqjg==",
"canShare": false,
"canViewPrivateItems": true,
"canEdit": false,
"allowedOnlineMeetingProviders": [],
"defaultOnlineMeetingProvider": "unknown",
"isTallyingResponses": false,
"isRemovable": true,
"owner": {
"name": "Abhay Soni",
"address": "abhaysoni.developer@gmail.com"
}
},
{
"id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAEGAACkUjuDFCV2Q4tYcM6WC3rQAAAZs65CAAA=",
"name": "Birthdays1",
"color": "auto",
"hexColor": "",
"isDefaultCalendar": false,
"changeKey": "pFI7gxQldkOLWHDOlgt60AAAGanrfQ==",
"canShare": false,
"canViewPrivateItems": true,
"canEdit": false,
"allowedOnlineMeetingProviders": [],
"defaultOnlineMeetingProvider": "unknown",
"isTallyingResponses": false,
"isRemovable": true,
"owner": {
"name": "Abhay Soni",
"address": "abhaysoni.developer@gmail.com"
}
},
{
"id": "AQMkADQyMDdlODU1AC05OGNiLTRhZmItOGM2Mi1kODZhMzJkYWU2OTAARgAAAwWXy9axQ31DtBuHDIFwgtYHAKRSO4MUJXZDi1hwzpYLetAAAAIBBgAAAKRSO4MUJXZDi1hwzpYLetAAAAIytQAAAA==",
"name": "Birthdays",
"color": "auto",
"hexColor": "#92e1c0",
"isDefaultCalendar": false,
"changeKey": "pFI7gxQldkOLWHDOlgt60AAAAAADdw==",
"canShare": false,
"canViewPrivateItems": true,
"canEdit": false,
"allowedOnlineMeetingProviders": [],
"defaultOnlineMeetingProvider": "unknown",
"isTallyingResponses": false,
"isRemovable": true,
"owner": {
"name": "Abhay Soni",
"address": "abhaysoni.developer@gmail.com"
}
},
{
"id": "AQMkADQyMDdlODU1AC05OGNiLTRhZmItOGM2Mi1kODZhMzJkYWU2OTAARgAAAwWXy9axQ31DtBuHDIFwgtYHAKRSO4MUJXZDi1hwzpYLetAAAAIBBgAAAKRSO4MUJXZDi1hwzpYLetAAAAIytgAAAA==",
"name": "Holidays in India",
"color": "auto",
"hexColor": "#16a765",
"isDefaultCalendar": false,
"changeKey": "pFI7gxQldkOLWHDOlgt60AAAAAADug==",
"canShare": false,
"canViewPrivateItems": true,
"canEdit": false,
"allowedOnlineMeetingProviders": [],
"defaultOnlineMeetingProvider": "unknown",
"isTallyingResponses": false,
"isRemovable": true,
"owner": {
"name": "Abhay Soni",
"address": "abhay.s@statusneo.com"
}
}
],
"events": [
{
"id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==",
"createdDateTime": "2022-12-22T08:28:36.6131501Z",
"lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z",
"changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==",
"categories": [],
"transactionId": null,
"originalStartTimeZone": "India Standard Time",
"originalEndTimeZone": "India Standard Time",
"iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89",
"reminderMinutesBeforeStart": 15,
"isReminderOn": true,
"hasAttachments": false,
"subject": "Abhay Soni",
"bodyPreview": "",
"importance": "normal",
"sensitivity": "normal",
"isAllDay": false,
"isCancelled": false,
"isOrganizer": false,
"responseRequested": true,
"seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=",
"showAs": "tentative",
"type": "occurrence",
"webLink": "",
"onlineMeetingUrl": "",
"isOnlineMeeting": true,
"onlineMeetingProvider": "teamsForBusiness",
"allowNewTimeProposals": true,
"occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16",
"isDraft": false,
"hideAttendees": false,
"responseStatus": {
"response": "notResponded",
"time": "0001-01-01T00:00:00Z"
},
"body": {
"contentType": "html",
"content": ""
},
"start": {
"dateTime": "2023-01-16T11:00:00.0000000",
"timeZone": "Asia/Calcutta"
},
"end": {
"dateTime": "2023-01-16T11:30:00.0000000",
"timeZone": "Asia/Calcutta"
},
"location": {
"displayName": "",
"locationType": "default",
"uniqueIdType": "unknown",
"address": {},
"coordinates": {}
},
"locations": [],
"recurrence": null,
"attendees": [
{
"type": "required",
"status": {
"response": "none",
"time": "0001-01-01T00:00:00Z"
},
"emailAddress": {
"name": "Nishkarsh Raj",
"address": "nishkarshraj@nishkarsh.com"
}
},
{
"type": "required",
"status": {
"response": "accepted",
"time": "0001-01-01T00:00:00Z"
},
"emailAddress": {
"name": "Hakam Ram",
"address": "hakam_ram@ram.com"
}
},
{
"type": "required",
"status": {
"response": "declined",
"time": "0001-01-01T00:00:00Z"
},
"emailAddress": {
"name": "Abhay Soni",
"address": "abhaysoni.developer@gmail.com"
}
}
],
"organizer": {
"emailAddress": {
"name": "Abhay Soni",
"address": "abhaysoni.developer@gmail.com"
}
},
"onlineMeeting": {
"joinUrl": "https://abhay-soni-developer.github.io/MyReusme/"
}
},
{
"id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==",
"createdDateTime": "2022-12-22T08:28:36.6131501Z",
"lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z",
"changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==",
"categories": [],
"transactionId": null,
"originalStartTimeZone": "India Standard Time",
"originalEndTimeZone": "India Standard Time",
"iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89",
"reminderMinutesBeforeStart": 15,
"isReminderOn": true,
"hasAttachments": false,
"subject": "https://abhay-soni-developer.github.io/MyReusme/",
"bodyPreview": "",
"importance": "normal",
"sensitivity": "normal",
"isAllDay": false,
"isCancelled": false,
"isOrganizer": false,
"responseRequested": true,
"seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=",
"showAs": "tentative",
"type": "occurrence",
"webLink": "",
"onlineMeetingUrl": "",
"isOnlineMeeting": true,
"onlineMeetingProvider": "teamsForBusiness",
"allowNewTimeProposals": true,
"occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16",
"isDraft": false,
"hideAttendees": false,
"responseStatus": {
"response": "notResponded",
"time": "0001-01-01T00:00:00Z"
},
"attendees": [
{
"type": "required",
"status": {
"response": "none",
"time": "0001-01-01T00:00:00Z"
},
"emailAddress": {
"name": "Nishkarsh Raj",
"address": "nishkarshraj@nishkarsh.com"
}
},
{
"type": "required",
"status": {
"response": "accepted",
"time": "0001-01-01T00:00:00Z"
},
"emailAddress": {
"name": "Hakam Ram",
"address": "hakam_ram@ram.com"
}
},
{
"type": "required",
"status": {
"response": "declined",
"time": "0001-01-01T00:00:00Z"
},
"emailAddress": {
"name": "Abhay Soni",
"address": "abhaysoni.developer@gmail.com"
}
}
],
"body": {
"contentType": "html",
"content": ""
},
"start": {
"dateTime": "2023-01-16T11:00:00.0000000",
"timeZone": "Asia/Calcutta"
},
"end": {
"dateTime": "2023-01-16T11:30:00.0000000",
"timeZone": "Asia/Calcutta"
},
"location": {
"displayName": "",
"locationType": "default",
"uniqueIdType": "unknown",
"address": {},
"coordinates": {}
},
"locations": [],
"recurrence": null,
"organizer": {
"emailAddress": {
"name": "Abhay Soni",
"address": "abhaysoni.developer@gmail.com"
}
},
"onlineMeeting": {
"joinUrl": "https://abhay-soni-developer.github.io/MyReusme/"
}
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+63
View File
@@ -0,0 +1,63 @@
{
"name": "@backstage/plugin-microsoft-calendar",
"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"
},
"backstage": {
"role": "frontend-plugin"
},
"author": {
"name": "Abhay Soni",
"email": "abhaysoni.developer@gmail.com",
"url": "https://github.com/Abhay-soni-developer"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@microsoft/microsoft-graph-types": "^2.25.0",
"@tanstack/react-query": "^4.1.3",
"classnames": "^2.3.1",
"dompurify": "^2.3.6",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
"material-ui-popup-state": "^1.9.3",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.49.0"
},
"files": [
"dist"
]
}
@@ -0,0 +1,95 @@
/*
* 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 { OAuthApi, createApiRef, FetchApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import { MicrosoftCalendarEvent, MicrosoftCalendar } from './types';
/** @public */
export const microsoftCalendarApiRef = createApiRef<MicrosoftCalendarApiClient>(
{
id: 'plugin.microsoft-calendar.service',
},
);
/** @public */
export class MicrosoftCalendarApiClient {
private readonly authApi: OAuthApi;
private readonly fetchApi: FetchApi;
constructor(options: { authApi: OAuthApi; fetchApi: FetchApi }) {
this.authApi = options.authApi;
this.fetchApi = options.fetchApi;
}
private async get<T>(
path: string,
params: { [key in string]: any } = {},
headers?: any,
): Promise<T> {
const query = new URLSearchParams(params);
const url = new URL(
`${path}?${query.toString()}`,
'https://graph.microsoft.com',
);
const token = await this.authApi.getAccessToken();
let temp: any = {};
if (headers && typeof headers === 'object') {
temp = {
...headers,
};
}
if (token) {
temp.Authorization = `Bearer ${token}`;
}
const response = await this.fetchApi.fetch(url.toString(), {
headers: temp,
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return response.json() as Promise<T>;
}
public async getCalendars(): Promise<MicrosoftCalendar[]> {
const data = await this.get<{
id: string;
value: MicrosoftCalendar[];
}>('v1.0/me/calendars');
return data.value;
}
public async getEvents(
calendarId: string,
params: {
startDateTime: string;
endDateTime: string;
},
headers: { [key in string]: any },
): Promise<MicrosoftCalendarEvent[]> {
const data = await this.get<{
id: string;
value: [MicrosoftCalendarEvent];
}>(`v1.0/me/calendars/${calendarId}/calendarview`, params, headers);
return data.value;
}
}
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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 * from './client';
export * from './types';
@@ -0,0 +1,40 @@
/*
* Copyright 2023 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 {
Calendar as MicrosoftCalendar,
Event as MicrosoftCalendarEvent,
Attendee,
ResponseStatus,
DateTimeTimeZone,
EmailAddress,
} from '@microsoft/microsoft-graph-types';
export type {
MicrosoftCalendar,
MicrosoftCalendarEvent,
Attendee,
ResponseStatus,
DateTimeTimeZone,
EmailAddress,
};
export const ResponseStatusMap = {
needsAction: 'none',
accepted: 'accepted',
declined: 'declined',
maybe: 'tentativelyAccepted',
notResponded: 'notResponded',
};
@@ -0,0 +1,89 @@
/*
* 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 { Attendee, ResponseStatusMap } from '../api';
const useStyles = makeStyles((theme: BackstageTheme) => {
const getIconColor = (responseStatus?: string) => {
if (!responseStatus) return theme.palette.primary.light;
return {
[ResponseStatusMap.accepted]: theme.palette.status.ok,
[ResponseStatusMap.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 }: { responseStatus: string }) => {
if (responseStatus === ResponseStatusMap.accepted) {
return <CheckIcon data-testid="accepted-icon" />;
}
if (responseStatus === ResponseStatusMap.declined) {
return <CancelIcon data-testid="declined-icon" />;
}
return null;
};
type AttendeeChipProps = {
user: Attendee;
};
export const AttendeeChip = ({ user }: AttendeeChipProps) => {
const classes = useStyles({ responseStatus: user.status?.response || '' });
return (
<Badge
classes={{
root: classes.responseStatus,
badge: classes.badge,
}}
badgeContent={
<ResponseIcon responseStatus={user.status?.response || ''} />
}
>
<Chip
size="small"
variant="outlined"
label={user.emailAddress?.address}
color="primary"
/>
</Badge>
);
};
@@ -0,0 +1,141 @@
/*
* 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, { useState } from 'react';
import { InfoCard, Progress } from '@backstage/core-components';
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 } from '../hooks';
import calendarIcon from '../icons/calendar.svg';
import { CalendarEvent } from './CalendarEvent';
import { CalendarSelect } from './CalendarSelect';
import { SignInContent } from './SignInContent';
import { getStartDate } from './util';
import useAsync from 'react-use/lib/useAsync';
export const CalendarCard = () => {
const [date, setDate] = useState(DateTime.now());
const [selectedCalendarId, setSelectedCalendarId] = useState('');
const changeDay = (offset = 1) => {
setDate(prev => prev.plus({ day: offset }));
};
const { isSignedIn, isInitialized, signIn } = useSignIn();
useAsync(async () => signIn(true), [signIn]);
const {
isLoading: isCalendarLoading,
isFetching: isCalendarFetching,
data: calendars = [],
} = useCalendarsQuery({
enabled: isSignedIn,
});
const defaultCalendarId = calendars.find(c => c.isDefaultCalendar)?.id;
const { data: events, isLoading: isEventLoading } = useEventsQuery({
calendarId: selectedCalendarId || defaultCalendarId || '',
enabled: isSignedIn && calendars.length > 0,
timeMin: date.startOf('day').toISO(),
timeMax: date.endOf('day').toISO(),
timeZone: date.zoneName,
});
const showLoader =
(isCalendarLoading && isCalendarFetching) ||
isEventLoading ||
!isInitialized;
return (
<InfoCard
noPadding
title={
<Box display="flex" alignItems="center">
<Box height={32} width={32} mr={1}>
<img src={calendarIcon} alt="Microsoft 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}
selectedCalendarId={selectedCalendarId || defaultCalendarId}
setSelectedCalendarId={setSelectedCalendarId}
disabled={
(isCalendarFetching && isCalendarLoading) || !isSignedIn
}
/>
</>
) : (
<Typography variant="h6">Agenda</Typography>
)}
</Box>
}
deepLink={{
link: 'https://outlook.office.com/calendar/',
title: 'Go to Calendar',
}}
>
<Box>
{showLoader && (
<Box py={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.id}`} event={event} />
))}
</Box>
)}
</Box>
</InfoCard>
);
};
@@ -0,0 +1,166 @@
/*
* 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 { Link } from '@backstage/core-components';
import {
Box,
Paper,
Popover,
Tooltip,
Typography,
makeStyles,
} from '@material-ui/core';
import webcamIcon from '../icons/webcam.svg';
import { CalendarEventPopoverContent } from './CalendarEventPopoverContent';
import { MicrosoftCalendarEvent } from '../api';
import {
getOnlineMeetingLink,
getTimePeriod,
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: {
width: 8,
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
}),
{
name: 'MicrosoftCalendarEvent',
},
);
export const CalendarEvent = ({ event }: { event: MicrosoftCalendarEvent }) => {
const classes = useStyles();
const popoverState = usePopupState({
variant: 'popover',
popupId: event.id,
disableAutoFocus: true,
});
const [hovered, setHovered] = useState(false);
const onlineMeetingLink = getOnlineMeetingLink(event);
const { onClick, ...restBindProps } = bindTrigger(popoverState);
return (
<>
<Paper
onClick={e => {
onClick(e);
}}
{...restBindProps}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
elevation={hovered ? 4 : 1}
className={classnames(classes.event, {
[classes.passed]: isPassed(event),
})}
data-testid="microsoft-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.isCancelled })}
>
{event.subject}
</Typography>
{!isAllDay(event) && (
<Typography variant="body2" data-testid="calendar-event-time">
{getTimePeriod(event)}
</Typography>
)}
</Box>
{event.isOnlineMeeting && (
<Tooltip title="Join Online Meeting">
<Link
data-testid="calendar-event-online-meeting-link"
className={classes.link}
to={onlineMeetingLink}
onClick={e => {
e.stopPropagation();
}}
noTrack
>
{/* we can use onlineMeetingProvider to show icon accordingly */}
<img
height={32}
width={32}
src={webcamIcon}
alt="Online Meeting 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,126 @@
/*
* 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 DOMPurify from 'dompurify';
import { Link } from '@backstage/core-components';
import {
Box,
Divider,
IconButton,
Tooltip,
Typography,
makeStyles,
} from '@material-ui/core';
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
import { AttendeeChip } from './AttendeeChip';
import { MicrosoftCalendarEvent } from '../api';
import { getTimePeriod, getOnlineMeetingLink } from './util';
const useStyles = makeStyles(
theme => ({
description: {
wordBreak: 'break-word',
'& a': {
color: theme.palette.primary.main,
fontWeight: 500,
},
},
divider: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
}),
{
name: 'MicrosoftCalendarEventPopoverContent',
},
);
type CalendarEventPopoverProps = {
event: MicrosoftCalendarEvent;
};
export const CalendarEventPopoverContent = ({
event,
}: CalendarEventPopoverProps) => {
const classes = useStyles();
const onlineMeetingLink = getOnlineMeetingLink(event);
return (
<Box display="flex" flexDirection="column" width={400} p={2}>
<Box display="flex" alignItems="center">
<Box flex={1}>
<Typography variant="h6">{event.subject}</Typography>
<Typography variant="subtitle2">{getTimePeriod(event)}</Typography>
</Box>
{event.webLink && (
<Tooltip title="Open in Calendar">
<Link
data-testid="open-calendar-link"
to={event.webLink}
onClick={_e => {}}
noTrack
>
<IconButton>
<ArrowForwardIcon />
</IconButton>
</Link>
</Tooltip>
)}
</Box>
{onlineMeetingLink && (
<Link to={onlineMeetingLink} onClick={_e => {}} noTrack>
Join Online Meeting
</Link>
)}
{event.bodyPreview && (
<>
<Divider className={classes.divider} variant="fullWidth" />
<Box
className={classes.description}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
(event.body && event.body.content) || '',
{
USE_PROFILES: { html: true },
},
),
}}
/>
</>
)}
{event.attendees && (
<>
<Divider className={classes.divider} variant="fullWidth" />
<Box>
<Typography variant="subtitle2">Attendees</Typography>
<Box mb={1} />
{sortBy(event.attendees || [], 'emailAddress').map(user => (
<AttendeeChip
key={user.emailAddress?.address || ''}
user={user}
/>
))}
</Box>
</>
)}
</Box>
);
};
@@ -0,0 +1,92 @@
/*
* 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 { MicrosoftCalendar } from '../api';
const useStyles = makeStyles(
{
formControl: {
width: 120,
},
selectedCalendars: {
textOverflow: 'ellipsis',
overflow: 'hidden',
},
},
{
name: 'MicrosoftCalendarSelect',
},
);
type CalendarSelectProps = {
disabled: boolean;
selectedCalendarId?: string;
setSelectedCalendarId: (value: string) => void;
calendars: MicrosoftCalendar[];
};
export const CalendarSelect = ({
disabled,
selectedCalendarId,
setSelectedCalendarId,
calendars,
}: CalendarSelectProps) => {
const classes = useStyles();
return (
<FormControl className={classes.formControl}>
<Select
labelId="calendars-label"
disabled={disabled || calendars.length === 0}
value={selectedCalendarId || ''}
onChange={async e => setSelectedCalendarId(e.target.value as string)}
input={<Input />}
renderValue={selected => {
return (
<Typography className={classes.selectedCalendars} variant="body2">
{calendars.find(c => c.id === selected)?.name}
</Typography>
);
}}
MenuProps={{
PaperProps: {
style: {
width: 350,
},
},
}}
>
{sortBy(calendars, 'name').map(c => (
<MenuItem key={c.id} value={c.id}>
<Checkbox checked={c.id === selectedCalendarId} />
<ListItemText primary={c.name} />
</MenuItem>
))}
</Select>
</FormControl>
);
};
@@ -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 '@tanstack/react-query';
import { CalendarCard } from './CalendarCard';
const queryClient = new QueryClient();
export const MicrosoftCalendar = () => {
return (
<QueryClientProvider client={queryClient}>
<CalendarCard />
</QueryClientProvider>
);
};
@@ -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 mockEvents from './eventMock.json';
import { MicrosoftCalendarEvent } from '../api';
type Props = {
handleAuthClick: React.MouseEventHandler<HTMLElement>;
};
const TransparentBox = styled(Box)({
opacity: 0.3,
filter: 'blur(1.5px)',
});
export const SignInContent = ({ handleAuthClick }: Props) => {
return (
<Box position="relative" height="100%" width="100%">
<TransparentBox p={1}>
{(mockEvents as MicrosoftCalendarEvent[]).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,138 @@
[
{
"id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==",
"createdDateTime": "2022-12-22T08:28:36.6131501Z",
"lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z",
"changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==",
"categories": [],
"transactionId": null,
"originalStartTimeZone": "India Standard Time",
"originalEndTimeZone": "India Standard Time",
"iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89",
"reminderMinutesBeforeStart": 15,
"isReminderOn": true,
"hasAttachments": false,
"subject": "Abhay Soni",
"bodyPreview": "",
"importance": "normal",
"sensitivity": "normal",
"isAllDay": false,
"isCancelled": false,
"isOrganizer": false,
"responseRequested": true,
"seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=",
"showAs": "tentative",
"type": "occurrence",
"webLink": "",
"onlineMeetingUrl": "",
"isOnlineMeeting": true,
"onlineMeetingProvider": "teamsForBusiness",
"allowNewTimeProposals": true,
"occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16",
"isDraft": false,
"hideAttendees": false,
"responseStatus": {
"response": "notResponded",
"time": "0001-01-01T00:00:00Z"
},
"body": {
"contentType": "html",
"content": ""
},
"start": {
"dateTime": "2023-01-16T11:00:00.0000000",
"timeZone": "Asia/Calcutta"
},
"end": {
"dateTime": "2023-01-16T11:30:00.0000000",
"timeZone": "Asia/Calcutta"
},
"location": {
"displayName": "",
"locationType": "default",
"uniqueIdType": "unknown",
"address": {},
"coordinates": {}
},
"locations": [],
"recurrence": null,
"attendees": [],
"organizer": {
"emailAddress": {
"name": "Abhay Soni",
"address": "abhaysoni.developer@gmail.com"
}
},
"onlineMeeting": {
"joinUrl": "https://abhay-soni-developer.github.io/MyReusme/"
}
},
{
"id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==",
"createdDateTime": "2022-12-22T08:28:36.6131501Z",
"lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z",
"changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==",
"categories": [],
"transactionId": null,
"originalStartTimeZone": "India Standard Time",
"originalEndTimeZone": "India Standard Time",
"iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89",
"reminderMinutesBeforeStart": 15,
"isReminderOn": true,
"hasAttachments": false,
"subject": "https://abhay-soni-developer.github.io/MyReusme/",
"bodyPreview": "",
"importance": "normal",
"sensitivity": "normal",
"isAllDay": false,
"isCancelled": false,
"isOrganizer": false,
"responseRequested": true,
"seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=",
"showAs": "tentative",
"type": "occurrence",
"webLink": "",
"onlineMeetingUrl": "",
"isOnlineMeeting": true,
"onlineMeetingProvider": "teamsForBusiness",
"allowNewTimeProposals": true,
"occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16",
"isDraft": false,
"hideAttendees": false,
"responseStatus": {
"response": "notResponded",
"time": "0001-01-01T00:00:00Z"
},
"body": {
"contentType": "html",
"content": ""
},
"start": {
"dateTime": "2023-01-16T11:00:00.0000000",
"timeZone": "Asia/Calcutta"
},
"end": {
"dateTime": "2023-01-16T11:30:00.0000000",
"timeZone": "Asia/Calcutta"
},
"location": {
"displayName": "",
"locationType": "default",
"uniqueIdType": "unknown",
"address": {},
"coordinates": {}
},
"locations": [],
"recurrence": null,
"attendees": [],
"organizer": {
"emailAddress": {
"name": "Abhay Soni",
"address": "abhaysoni.developer@gmail.com"
}
},
"onlineMeeting": {
"joinUrl": "https://abhay-soni-developer.github.io/MyReusme/"
}
}
]
@@ -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 { MicrosoftCalendar } from './MicrosoftCalendar';
@@ -0,0 +1,76 @@
/*
* 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 { MicrosoftCalendarEvent } from '../api';
export function getOnlineMeetingLink(event: MicrosoftCalendarEvent) {
const onlineEntrypoint =
event.onlineMeeting?.joinUrl || event.onlineMeetingUrl;
if (onlineEntrypoint) {
return onlineEntrypoint;
}
return '';
}
export function getTimePeriod(event: MicrosoftCalendarEvent) {
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: MicrosoftCalendarEvent) {
const format: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric' };
const startTime = DateTime.fromISO(event.start?.dateTime || '');
const endTime = DateTime.fromISO(event.end?.dateTime || '').minus({ day: 1 });
if (startTime.toISO() === endTime.toISO()) {
return startTime.toLocaleString(format);
}
return `${startTime.toLocaleString(format)} - ${endTime.toLocaleString(
format,
)}`;
}
export function isPassed(event: MicrosoftCalendarEvent) {
if (!event.end?.dateTime) return false;
const eventDate = DateTime.fromISO(event.end?.dateTime!);
return DateTime.now() >= eventDate;
}
export function isAllDay(event: MicrosoftCalendarEvent) {
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: MicrosoftCalendarEvent) {
return DateTime.fromISO(event.start?.dateTime || '');
}
@@ -0,0 +1,18 @@
/*
* 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';
@@ -0,0 +1,52 @@
/*
* 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 '@tanstack/react-query';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { microsoftCalendarApiRef } from '../api';
type Options = {
enabled: boolean;
refreshTime?: number;
};
export const useCalendarsQuery = ({ enabled }: Options) => {
const calendarApi = useApi(microsoftCalendarApiRef);
const errorApi = useApi(errorApiRef);
return useQuery(
['calendars-list'],
async () => {
const calendars = [];
const value = await calendarApi.getCalendars();
calendars.push(...value);
return calendars;
},
{
enabled,
// old data will be returned if last request was made within 3 mins.
staleTime: 180000,
refetchInterval: 3600000,
onError: () => {
errorApi.post({
name: 'API error',
message: 'Failed to fetch calendars.',
});
},
},
);
};
@@ -0,0 +1,61 @@
/*
* 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 '@tanstack/react-query';
import { useApi } from '@backstage/core-plugin-api';
import { microsoftCalendarApiRef } from '../api';
type Options = {
calendarId: string;
timeMin: string;
timeMax: string;
enabled?: boolean;
timeZone?: string;
refreshTime?: number;
};
export const useEventsQuery = ({
calendarId,
enabled,
timeMin,
timeMax,
timeZone,
}: Options) => {
const calendarApi = useApi(microsoftCalendarApiRef);
return useQuery(
['calendarEvents', calendarId, timeMin, timeMax, timeZone],
async () => {
const data = await calendarApi.getEvents(
calendarId,
{
startDateTime: timeMin,
endDateTime: timeMax,
},
{
Prefer: `outlook.timezone="${timeZone}"`,
},
);
return data;
},
{
cacheTime: 300000,
enabled,
refetchInterval: 60000,
refetchIntervalInBackground: true,
},
);
};
@@ -0,0 +1,39 @@
/*
* 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 { microsoftAuthApiRef, useApi } from '@backstage/core-plugin-api';
export const useSignIn = () => {
const [isSignedIn, setSignedIn] = useState(false);
const [isInitialized, setInitialized] = useState(false);
const authApi = useApi(microsoftAuthApiRef);
const signIn = useCallback(
async (optional = false) => {
const token = await authApi.getAccessToken('Calendars.Read', {
optional,
instantPopup: !optional,
});
setSignedIn(!!token);
setInitialized(true);
},
[authApi, setSignedIn],
);
return { isSignedIn, isInitialized, signIn };
};
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<circle style="fill:#45B39C;" cx="256" cy="256" r="256"/>
<path style="opacity:0.1;enable-background:new ;" d="M512,256c0-10.816-0.747-21.445-2.048-31.904L399.781,113.925l-0.107-0.107
c-2.24-2.773-5.707-4.48-9.547-4.48H121.861c-6.933,0-12.533,5.6-12.533,12.533v268.267c0,3.253,1.227,6.187,3.253,8.373
l111.424,111.424C234.496,511.248,245.157,512,256,512C397.387,512,512,397.387,512,256z"/>
<path style="fill:#E56353;" d="M390.133,109.333H121.867c-6.923,0-12.533,5.611-12.533,12.533v42.277h293.333v-42.277
C402.667,114.944,397.056,109.333,390.133,109.333z"/>
<path style="fill:#D5D6DB;" d="M109.333,164.144v225.989c0,6.923,5.611,12.533,12.533,12.533h268.267
c6.923,0,12.533-5.611,12.533-12.533V164.144H109.333z"/>
<path style="fill:#EBF0F3;" d="M109.333,164.144v215.323c0,6.923,5.611,12.533,12.533,12.533h268.267
c6.923,0,12.533-5.611,12.533-12.533V164.144H109.333z"/>
<circle style="fill:#D15241;" cx="144.8" cy="150.827" r="7.675"/>
<circle style="fill:#D5D6DB;" cx="144.8" cy="177.493" r="7.675"/>
<path style="fill:#64798A;" d="M144.789,145.771c-2.917,0-5.285,2.368-5.285,5.285v26.176c0,2.917,2.368,5.285,5.285,5.285
c2.917,0,5.285-2.368,5.285-5.285v-26.176C150.069,148.139,147.707,145.771,144.789,145.771z"/>
<circle style="fill:#D15241;" cx="367.216" cy="150.827" r="7.675"/>
<circle style="fill:#D5D6DB;" cx="367.216" cy="177.493" r="7.675"/>
<path style="fill:#64798A;" d="M367.216,145.771c-2.917,0-5.285,2.368-5.285,5.285v26.176c0,2.917,2.363,5.285,5.285,5.285
c2.917,0,5.285-2.368,5.285-5.285v-26.176C372.496,148.139,370.133,145.771,367.216,145.771z"/>
<circle style="fill:#D15241;" cx="293.072" cy="150.827" r="7.675"/>
<circle style="fill:#D5D6DB;" cx="293.072" cy="177.493" r="7.675"/>
<path style="fill:#64798A;" d="M293.072,145.771c-2.917,0-5.285,2.368-5.285,5.285v26.176c0,2.917,2.363,5.285,5.285,5.285
c2.917,0,5.285-2.368,5.285-5.285v-26.176C298.357,148.139,295.989,145.771,293.072,145.771z"/>
<circle style="fill:#D15241;" cx="218.933" cy="150.827" r="7.675"/>
<circle style="fill:#D5D6DB;" cx="218.933" cy="177.493" r="7.675"/>
<path style="fill:#64798A;" d="M218.928,145.771c-2.917,0-5.285,2.368-5.285,5.285v26.176c0,2.917,2.363,5.285,5.285,5.285
c2.917,0,5.285-2.368,5.285-5.285v-26.176C224.213,148.139,221.845,145.771,218.928,145.771z"/>
<rect x="218.491" y="210.437" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
<rect x="265.125" y="210.437" style="fill:#44C4A1;" width="28.384" height="28.384"/>
<rect x="311.733" y="210.437" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
<rect x="358.4" y="210.437" style="fill:#E56353;" width="28.384" height="28.384"/>
<g>
<rect x="125.221" y="255.019" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
<rect x="171.84" y="255.019" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
<rect x="218.491" y="255.019" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
</g>
<rect x="265.125" y="255.019" style="fill:#44C4A1;" width="28.384" height="28.384"/>
<rect x="311.733" y="255.019" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
<rect x="358.4" y="255.019" style="fill:#E56353;" width="28.384" height="28.384"/>
<g>
<rect x="125.221" y="299.573" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
<rect x="171.84" y="299.573" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
<rect x="218.491" y="299.573" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
</g>
<rect x="265.125" y="299.573" style="fill:#44C4A1;" width="28.384" height="28.384"/>
<rect x="311.733" y="299.573" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
<rect x="358.4" y="299.573" style="fill:#E56353;" width="28.384" height="28.384"/>
<g>
<rect x="125.221" y="344.16" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
<rect x="171.84" y="344.16" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
<rect x="218.491" y="344.16" style="fill:#D5D6DB;" width="28.384" height="28.384"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M778.666667 938.666667h-533.333334c-23.466667 0-38.4-25.6-27.733333-46.933334L277.333333 789.333333h469.333334l57.6 102.4c12.8 21.333333-2.133333 46.933333-25.6 46.933334z" fill="#455A64" /><path d="M512 490.666667m-384 0a384 384 0 1 0 768 0 384 384 0 1 0-768 0Z" fill="#78909C" /><path d="M512 746.666667c-140.8 0-256-115.2-256-256s115.2-256 256-256 256 115.2 256 256-115.2 256-256 256z" fill="#455A64" /><path d="M512 490.666667m-192 0a192 192 0 1 0 384 0 192 192 0 1 0-384 0Z" fill="#42A5F5" /><path d="M614.4 426.666667c-25.6-29.866667-64-46.933333-102.4-46.933334s-76.8 17.066667-102.4 46.933334c-10.666667 10.666667-8.533333 27.733333 2.133333 38.4s27.733333 8.533333 38.4-2.133334c32-36.266667 91.733333-36.266667 123.733334 0 6.4 6.4 12.8 8.533333 21.333333 8.533334 6.4 0 12.8-2.133333 19.2-6.4 8.533333-8.533333 10.666667-27.733333 0-38.4z" fill="#90CAF9" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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 { MicrosoftCalendarCard } from './plugin';
export { MicrosoftCalendarApiClient, microsoftCalendarApiRef } from './api';
@@ -0,0 +1,22 @@
/*
* Copyright 2023 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 { microsoftCalendarPlugin } from './plugin';
describe('microsoft-calendar', () => {
it('should export plugin', () => {
expect(microsoftCalendarPlugin).toBeDefined();
});
});
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright 2023 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 {
createPlugin,
microsoftAuthApiRef,
fetchApiRef,
createApiFactory,
createComponentExtension,
} from '@backstage/core-plugin-api';
import {
microsoftCalendarApiRef,
MicrosoftCalendarApiClient,
} from './api/index';
export const microsoftCalendarPlugin = createPlugin({
id: 'microsoft-calendar',
apis: [
createApiFactory({
api: microsoftCalendarApiRef,
deps: { authApi: microsoftAuthApiRef, fetchApi: fetchApiRef },
factory(deps) {
return new MicrosoftCalendarApiClient(deps);
},
}),
],
});
/** @public */
export const MicrosoftCalendarCard = microsoftCalendarPlugin.provide(
createComponentExtension({
name: 'MicrosoftCalendarCard',
component: {
lazy: () => import('./components').then(m => m.MicrosoftCalendar),
},
}),
);
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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';
+35 -1
View File
@@ -6895,6 +6895,39 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-microsoft-calendar@workspace:^, @backstage/plugin-microsoft-calendar@workspace:plugins/microsoft-calendar":
version: 0.0.0-use.local
resolution: "@backstage/plugin-microsoft-calendar@workspace:plugins/microsoft-calendar"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.12.2
"@material-ui/icons": ^4.9.1
"@microsoft/microsoft-graph-types": ^2.25.0
"@tanstack/react-query": ^4.1.3
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@testing-library/user-event": ^14.0.0
"@types/node": ^16.11.26
classnames: ^2.3.1
cross-fetch: ^3.1.5
dompurify: ^2.3.6
lodash: ^4.17.21
luxon: ^3.0.0
material-ui-popup-state: ^1.9.3
msw: ^0.49.0
react-use: ^17.2.4
peerDependencies:
react: ^16.13.1 || ^17.0.0
languageName: unknown
linkType: soft
"@backstage/plugin-newrelic-dashboard@workspace:^, @backstage/plugin-newrelic-dashboard@workspace:plugins/newrelic-dashboard":
version: 0.0.0-use.local
resolution: "@backstage/plugin-newrelic-dashboard@workspace:plugins/newrelic-dashboard"
@@ -11514,7 +11547,7 @@ __metadata:
languageName: node
linkType: hard
"@microsoft/microsoft-graph-types@npm:^2.6.0":
"@microsoft/microsoft-graph-types@npm:^2.25.0, @microsoft/microsoft-graph-types@npm:^2.6.0":
version: 2.25.0
resolution: "@microsoft/microsoft-graph-types@npm:2.25.0"
checksum: b311385e0a51f827082af5a016f35b1d6cd4d861121e8767b3279ab1a17475977cddbbdcc208abdddf0f953fd6ce4634d3c302e317cf50af4cb6111385347b4f
@@ -22194,6 +22227,7 @@ __metadata:
"@backstage/plugin-kafka": "workspace:^"
"@backstage/plugin-kubernetes": "workspace:^"
"@backstage/plugin-lighthouse": "workspace:^"
"@backstage/plugin-microsoft-calendar": "workspace:^"
"@backstage/plugin-newrelic": "workspace:^"
"@backstage/plugin-newrelic-dashboard": "workspace:^"
"@backstage/plugin-org": "workspace:^"