diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 20eabbcac5..610cd7f159 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -34,6 +34,7 @@ import {
SidebarUserBadge,
SidebarThemeToggle,
} from '@backstage/core';
+import { NavLink } from 'react-router-dom';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -56,9 +57,11 @@ const SidebarLogo: FC<{}> = () => {
return (
-
- {isOpen ? : }
-
+
+
+ {isOpen ? : }
+
+
);
};
diff --git a/packages/backend/README.md b/packages/backend/README.md
index d1cc8b99b7..061c0d51a5 100644
--- a/packages/backend/README.md
+++ b/packages/backend/README.md
@@ -15,6 +15,7 @@ app. Until then, feel free to experiment here!
To run the example backend, first go to the project root and run
```bash
+yarn tsc
yarn install
yarn build
```
diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts
new file mode 100644
index 0000000000..ab7a90249a
--- /dev/null
+++ b/packages/catalog-model/src/location/annotation.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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 const LOCATION_ANNOTATION = 'backstage.io/managed-by-location';
diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts
index 60465bff9b..ff696524ab 100644
--- a/packages/catalog-model/src/location/index.ts
+++ b/packages/catalog-model/src/location/index.ts
@@ -16,3 +16,4 @@
export type { Location, LocationSpec } from './types';
export { locationSchema, locationSpecSchema } from './validation';
+export { LOCATION_ANNOTATION } from './annotation';
diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts
index 2d592bc549..3516c45b9b 100644
--- a/packages/cli/src/lib/tasks.ts
+++ b/packages/cli/src/lib/tasks.ts
@@ -143,6 +143,7 @@ export async function installWithLocalDeps(dir: string) {
// Add to both resolutions and dependencies, or transitive dependencies will still be fetched from the registry.
pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`;
pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`;
+ delete pkgJson.devDependencies[`@backstage/${name}`];
await fs
.writeJSON(pkgJsonPath, pkgJson, { encoding: 'utf8', spaces: 2 })
diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts
index c066c83b73..4cbc8bcfb1 100644
--- a/packages/core-api/src/apis/definitions/auth.ts
+++ b/packages/core-api/src/apis/definitions/auth.ts
@@ -143,6 +143,30 @@ export type OpenIdConnectApi = {
logout(): Promise;
};
+export type ProfileInfoOptions = {
+ /**
+ * If this is set to true, the user will not be prompted to log in,
+ * and an empty profile will be returned if there is no existing session.
+ *
+ * This can be used to perform a check whether the user is logged in, or if you don't
+ * want to force a user to be logged in, but provide functionality if they already are.
+ *
+ * @default false
+ */
+ optional?: boolean;
+};
+
+export type ProfileInfoApi = {
+ getProfile(options?: ProfileInfoOptions): Promise;
+};
+
+export type ProfileInfo = {
+ provider: string;
+ email: string;
+ name?: string;
+ picture?: string;
+};
+
/**
* Provides authentication towards Google APIs and identities.
*
@@ -151,7 +175,9 @@ export type OpenIdConnectApi = {
* Note that the ID token payload is only guaranteed to contain the user's numerical Google ID,
* email and expiration information. Do not rely on any other fields, as they might not be present.
*/
-export const googleAuthApiRef = createApiRef({
+export const googleAuthApiRef = createApiRef<
+ OAuthApi & OpenIdConnectApi & ProfileInfoApi
+>({
id: 'core.auth.google',
description: 'Provides authentication towards Google APIs and identities',
});
diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts
index 582afbcfea..d65a2c71ee 100644
--- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts
+++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts
@@ -22,6 +22,9 @@ import {
OpenIdConnectApi,
IdTokenOptions,
AccessTokenOptions,
+ ProfileInfoApi,
+ ProfileInfoOptions,
+ ProfileInfo,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
@@ -39,6 +42,7 @@ type CreateOptions = {
};
export type GoogleAuthResponse = {
+ profile: ProfileInfo;
accessToken: string;
idToken: string;
scope: string;
@@ -53,7 +57,7 @@ const DEFAULT_PROVIDER = {
const SCOPE_PREFIX = 'https://www.googleapis.com/auth/';
-class GoogleAuth implements OAuthApi, OpenIdConnectApi {
+class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi {
static create({
apiOrigin,
basePath,
@@ -69,6 +73,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi {
oauthRequestApi: oauthRequestApi,
sessionTransform(res: GoogleAuthResponse): GoogleSession {
return {
+ profile: res.profile,
idToken: res.idToken,
accessToken: res.accessToken,
scopes: GoogleAuth.normalizeScopes(res.scope),
@@ -123,6 +128,14 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi {
await this.sessionManager.removeSession();
}
+ async getProfile(options: ProfileInfoOptions = {}) {
+ const session = await this.sessionManager.getSession(options);
+ if (!session) {
+ return undefined;
+ }
+ return session.profile;
+ }
+
static normalizeScopes(scopes?: string | string[]): Set {
if (!scopes) {
return new Set();
diff --git a/packages/core-api/src/apis/implementations/auth/google/types.ts b/packages/core-api/src/apis/implementations/auth/google/types.ts
index 96c69c5d4f..ea251c7006 100644
--- a/packages/core-api/src/apis/implementations/auth/google/types.ts
+++ b/packages/core-api/src/apis/implementations/auth/google/types.ts
@@ -14,7 +14,10 @@
* limitations under the License.
*/
+import { ProfileInfo } from '../../../definitions';
+
export type GoogleSession = {
+ profile: ProfileInfo;
idToken: string;
accessToken: string;
scopes: Set;
diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
index 64df3deca4..3df21af3f3 100644
--- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
+++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
@@ -117,6 +117,10 @@ export class RefreshingAuthSessionManager implements SessionManager {
window.location.reload(); // TODO(Rugvip): make this work without reload?
}
+ async getCurrentSession() {
+ return this.currentSession;
+ }
+
private async collapsedSessionRefresh(): Promise {
if (this.refreshPromise) {
return this.refreshPromise;
diff --git a/packages/core/src/layout/HeaderTabs/index.tsx b/packages/core/src/layout/HeaderTabs/index.tsx
new file mode 100644
index 0000000000..158cd76913
--- /dev/null
+++ b/packages/core/src/layout/HeaderTabs/index.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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.
+ */
+
+// TODO(blam): Remove this implementation when the Tabs are ready
+// This is just a temporary solution to implementing tabs for now
+
+import React from 'react';
+import { makeStyles, Tabs, Tab } from '@material-ui/core';
+
+const useStyles = makeStyles(theme => ({
+ tabsWrapper: {
+ gridArea: 'pageSubheader',
+ backgroundColor: theme.palette.background.paper,
+ },
+ defaultTab: {
+ padding: theme.spacing(3, 3),
+ ...theme.typography.caption,
+ textTransform: 'uppercase',
+ fontWeight: 'bold',
+ color: theme.palette.text.secondary,
+ },
+ selected: {
+ color: theme.palette.text.primary,
+ },
+}));
+
+export type Tab = {
+ id: string;
+ label: string;
+};
+export const HeaderTabs: React.FC<{ tabs: Tab[] }> = ({ tabs }) => {
+ const styles = useStyles();
+
+ return (
+
+
+ {tabs.map((tab, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx b/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx
index f9bd4ed2ab..d0c3dc62c2 100644
--- a/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx
+++ b/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx
@@ -14,48 +14,285 @@
* limitations under the License.
*/
-import React, { FC } from 'react';
+import React, { FC, useState, useEffect } from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { sidebarConfig } from './config';
-import { Avatar, Typography } from '@material-ui/core';
+import {
+ Avatar,
+ ListItem,
+ ListItemAvatar,
+ ListItemText,
+ Popover,
+ List,
+ ListItemIcon,
+ ListItemSecondaryAction,
+ IconButton,
+ Tooltip,
+ Typography,
+} from '@material-ui/core';
+import { blueGrey } from '@material-ui/core/colors';
+import { useSetState } from 'react-use';
+import { Skeleton } from '@material-ui/lab';
+import { useApi, googleAuthApiRef, ProfileInfo } from '@backstage/core-api';
+import LogoutIcon from '@material-ui/icons/PowerSettingsNew';
+import ControlPointIcon from '@material-ui/icons/ControlPoint';
+import AccountCircleIcon from '@material-ui/icons/AccountCircle';
-const useStyles = makeStyles(() => {
+const useStyles = makeStyles(theme => {
const { drawerWidthOpen, userBadgeDiameter } = sidebarConfig;
return {
root: {
width: drawerWidthOpen,
display: 'flex',
alignItems: 'center',
- color: '#b5b5b5',
paddingLeft: 18,
paddingTop: 14,
paddingBottom: 14,
+ color: '#b5b5b5',
},
avatar: {
width: userBadgeDiameter,
height: userBadgeDiameter,
marginRight: 8,
},
+ purple: {
+ color: theme.palette.getContrastText(blueGrey[500]),
+ backgroundColor: blueGrey[500],
+ },
+ listItemText: {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ },
};
});
+const SessionListItem: FC<{
+ classes: any;
+ loading: boolean;
+ title: string;
+ icon: any;
+ user: any;
+ onSignIn: Function;
+ onSignOut: Function;
+}> = ({
+ classes,
+ loading,
+ title,
+ icon,
+ user,
+ onSignIn,
+ onSignOut,
+ ...props
+}) => {
+ if (loading) {
+ return (
+
+
+
+
+ }
+ secondary={ }
+ />
+
+
+
+
+
+
+ );
+ }
+
+ // TODO: Not functional yet to sign in from the sidebar
+ if (!user) {
+ return (
+
+ {icon}
+
+
+
+ onSignIn()}>
+
+
+
+
+
+ );
+ }
+
+ const { id, avatarUrl, avatarAlt } = user;
+
+ return (
+
+
+
+ {avatarAlt && avatarAlt[0].toUpperCase()}
+
+
+
+ {id}
+
+ }
+ secondary={title}
+ />
+
+
+ onSignOut()}>
+
+
+
+
+
+ );
+};
+
+const useGoogleLoginState = (open: boolean) => {
+ const googleAuth = useApi(googleAuthApiRef);
+ const [loading, setLoading] = useState(true);
+ const [profile, setProfile] = useState();
+
+ useEffect(() => {
+ let didCancel = false;
+
+ if (open) {
+ googleAuth.getProfile().then(_profile => {
+ if (!didCancel) {
+ setProfile(_profile);
+ setLoading(false);
+ }
+ });
+ }
+
+ return () => {
+ didCancel = true;
+ };
+ }, [open, googleAuth]);
+
+ if (loading) {
+ return { loading: true };
+ }
+ return { loading: false, isLoggedIn: !!profile, profile };
+};
+
type Props = {
- imageUrl: string;
- name: string;
- hideName?: boolean;
+ email: string;
+ imageUrl?: string;
+ name?: string;
+ collapsedMode?: boolean;
};
export const LoggedUserBadge: FC = ({
imageUrl,
name,
- hideName = false,
+ email,
+ collapsedMode = false,
}) => {
+ const [state, setState] = useSetState({
+ open: false,
+ anchorEl: null,
+ });
+ const googleAuth = useApi(googleAuthApiRef);
+ const googleLogin = useGoogleLoginState(state.open);
+
+ const handleOpen = (event: {
+ preventDefault: () => void;
+ currentTarget: any;
+ }) => {
+ // This prevents ghost click.
+ event.preventDefault();
+ setState({
+ open: true,
+ anchorEl: event.currentTarget,
+ });
+ };
+
+ const handleClose = () => {
+ setState({
+ open: false,
+ });
+ };
+
+ const handleGoogleSignIn = () => {
+ googleAuth.getIdToken();
+ handleClose();
+ };
+
+ const handleGoogleSignOut = () => {
+ googleAuth.logout();
+ };
+
const classes = useStyles();
+ const avatarFallback = email.charAt(0).toUpperCase() + email.slice(1);
+ const emailTrimmed = email.split('@')[0];
+ const displayEmail =
+ emailTrimmed.charAt(0).toUpperCase() + emailTrimmed.slice(1);
+ const displayName = name ?? displayEmail;
return (
-
-
- {!hideName &&
{name} }
-
+ <>
+
+
+
+ {imageUrl ? (
+
+ ) : (
+
+ {avatarFallback[0]}
+
+ )}
+
+ {!collapsedMode && (
+
+ {displayName}
+
+ }
+ />
+ )}
+
+
+
+
+
+
+
+ >
);
};
diff --git a/packages/core/src/layout/Sidebar/UserBadge.tsx b/packages/core/src/layout/Sidebar/UserBadge.tsx
index b030ed2996..d3e02d26cc 100644
--- a/packages/core/src/layout/Sidebar/UserBadge.tsx
+++ b/packages/core/src/layout/Sidebar/UserBadge.tsx
@@ -14,15 +14,16 @@
* limitations under the License.
*/
-import React, { FC, useContext } from 'react';
+import React, { FC, useContext, useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core';
-import People from '@material-ui/icons/People';
+import AccountCircleIcon from '@material-ui/icons/AccountCircle';
import { SidebarContext } from './config';
import { SidebarItem } from './Items';
import { LoggedUserBadge } from './LoggedUserBadge';
import DoubleArrowIcon from '@material-ui/icons/DoubleArrow';
import { BackstageTheme } from '@backstage/theme';
import { SidebarPinStateContext } from './Page';
+import { useApi, googleAuthApiRef, ProfileInfo } from '@backstage/core-api';
const ARROW_BUTTON_SIZE = 20;
const useStyles = makeStyles(theme => {
@@ -58,18 +59,30 @@ export const SidebarUserBadge: FC<{}> = () => {
SidebarPinStateContext,
);
const classes = useStyles({ isPinned });
+ const googleAuth = useApi(googleAuthApiRef);
+ const [profile, setProfile] = useState();
+
+ useEffect(() => {
+ // TODO(soapraj): How to observe if the user is logged in
+ // TODO(soapraj): List all the providers supported by the app and let user log in from here
+ googleAuth.getProfile({ optional: true }).then(googleProfile => {
+ setProfile(googleProfile);
+ });
+ }, [googleAuth]);
- const isUserLoggedIn = false;
return (
- {isUserLoggedIn ? (
-
+ {profile ? (
+ <>
+
+ >
) : (
-
+
)}
{isOpen && (
{
class MyCustomRefreshTokenSuccess extends passport.Strategy {
// @ts-ignore
private _oauth2 = new MyCustomOAuth2Success();
+ userProfile(_accessToken: string, callback: Function) {
+ callback(null, {
+ provider: 'a',
+ email: 'b',
+ name: 'c',
+ picture: 'd',
+ });
+ }
}
const mockStrategy = new MyCustomRefreshTokenSuccess();
diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts
index 5c02930c2c..7b7e467281 100644
--- a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts
+++ b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts
@@ -16,7 +16,43 @@
import express from 'express';
import passport from 'passport';
-import { RedirectInfo, RefreshTokenResponse } from './types';
+import jwtDecoder from 'jwt-decode';
+import { RedirectInfo, RefreshTokenResponse, ProfileInfo } from './types';
+
+export const makeProfileInfo = (
+ profile: passport.Profile,
+ params: any,
+): ProfileInfo => {
+ const { provider, displayName: name } = profile;
+
+ let email = '';
+ if (profile.emails) {
+ const [firstEmail] = profile.emails;
+ email = firstEmail.value;
+ }
+
+ if (!email && params.id_token) {
+ try {
+ const decoded: { email: string } = jwtDecoder(params.id_token);
+ email = decoded.email;
+ } catch (e) {
+ console.error('Failed to parse id token and get profile info');
+ }
+ }
+
+ let picture = '';
+ if (profile.photos) {
+ const [firstPhoto] = profile.photos;
+ picture = firstPhoto.value;
+ }
+
+ return {
+ provider,
+ name,
+ email,
+ picture,
+ };
+};
export const executeRedirectStrategy = async (
req: express.Request,
@@ -98,6 +134,7 @@ export const executeRefreshTokenStrategy = async (
),
);
}
+
resolve({
accessToken,
params,
@@ -106,3 +143,24 @@ export const executeRefreshTokenStrategy = async (
);
});
};
+
+export const executeFetchUserProfileStrategy = async (
+ providerstrategy: passport.Strategy,
+ accessToken: string,
+ params: any,
+): Promise => {
+ return new Promise((resolve, reject) => {
+ const anyStrategy = providerstrategy as any;
+ anyStrategy.userProfile(
+ accessToken,
+ (error: Error, passportProfile: passport.Profile) => {
+ if (error) {
+ reject(error);
+ }
+
+ const profile = makeProfileInfo(passportProfile, params);
+ resolve(profile);
+ },
+ );
+ });
+};
diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts
index e3969b14c8..066e16e330 100644
--- a/plugins/auth-backend/src/providers/google/provider.ts
+++ b/plugins/auth-backend/src/providers/google/provider.ts
@@ -20,6 +20,8 @@ import {
executeFrameHandlerStrategy,
executeRedirectStrategy,
executeRefreshTokenStrategy,
+ makeProfileInfo,
+ executeFetchUserProfileStrategy,
} from '../PassportStrategyHelper';
import {
OAuthProviderHandlers,
@@ -27,8 +29,10 @@ import {
AuthInfoPrivate,
RedirectInfo,
AuthProviderConfig,
+ AuthInfoWithProfile,
} from '../types';
import { OAuthProvider } from '../OAuthProvider';
+import passport from 'passport';
export class GoogleAuthProvider implements OAuthProviderHandlers {
private readonly providerConfig: AuthProviderConfig;
@@ -43,13 +47,14 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
accessToken: any,
refreshToken: any,
params: any,
- profile: any,
+ profile: passport.Profile,
done: any,
) => {
+ const profileInfo = makeProfileInfo(profile, params);
done(
undefined,
{
- profile,
+ profile: profileInfo,
idToken: params.id_token,
accessToken,
scope: params.scope,
@@ -73,18 +78,28 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
return await executeFrameHandlerStrategy(req, this._strategy);
}
- async refresh(refreshToken: string, scope: string): Promise {
+ async refresh(
+ refreshToken: string,
+ scope: string,
+ ): Promise {
const { accessToken, params } = await executeRefreshTokenStrategy(
this._strategy,
refreshToken,
scope,
);
+ const profile = await executeFetchUserProfileStrategy(
+ this._strategy,
+ accessToken,
+ params,
+ );
+
return {
accessToken,
idToken: params.id_token,
expiresInSeconds: params.expires_in,
scope: params.scope,
+ profile,
};
}
}
diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts
index 661435e74b..b8252ddc97 100644
--- a/plugins/auth-backend/src/providers/types.ts
+++ b/plugins/auth-backend/src/providers/types.ts
@@ -15,7 +15,6 @@
*/
import express from 'express';
-import passport from 'passport';
export type AuthProviderConfig = {
provider: string;
@@ -49,7 +48,14 @@ export type AuthInfoBase = {
};
export type AuthInfoWithProfile = AuthInfoBase & {
- profile: passport.Profile;
+ profile:
+ | {
+ provider: string;
+ email: string;
+ name?: string;
+ picture?: string;
+ }
+ | undefined;
};
export type AuthInfoPrivate = {
@@ -71,6 +77,13 @@ export type RedirectInfo = {
status?: number;
};
+export type ProfileInfo = {
+ provider: string;
+ email: string;
+ name: string;
+ picture: string;
+};
+
export type RefreshTokenResponse = {
accessToken: string;
params: any;
diff --git a/plugins/catalog-backend/fixtures/two_components.yaml b/plugins/catalog-backend/fixtures/two_components.yaml
index 470106116d..2fcacb8492 100644
--- a/plugins/catalog-backend/fixtures/two_components.yaml
+++ b/plugins/catalog-backend/fixtures/two_components.yaml
@@ -2,13 +2,13 @@
apiVersion: backstage.io/v1beta1
kind: Component
metadata:
- name: component1
+ name: playlist-proxy
spec:
type: service
---
apiVersion: backstage.io/v1beta1
kind: Component
metadata:
- name: component2
+ name: artist-web
spec:
type: website
diff --git a/plugins/catalog-backend/scripts/mock-data b/plugins/catalog-backend/scripts/mock-data
index 7edc8c422b..2342f09fa4 100755
--- a/plugins/catalog-backend/scripts/mock-data
+++ b/plugins/catalog-backend/scripts/mock-data
@@ -1,7 +1,7 @@
#!/usr/bin/env sh
curl \
--location \
---request POST 'localhost:3003/locations' \
+--request POST 'localhost:7000/catalog/locations' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "github",
diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts
index dd965c89ef..bb5b95d9ba 100644
--- a/plugins/catalog-backend/src/database/CommonDatabase.ts
+++ b/plugins/catalog-backend/src/database/CommonDatabase.ts
@@ -19,7 +19,12 @@ import {
InputError,
NotFoundError,
} from '@backstage/backend-common';
-import type { Entity, EntityMeta, Location } from '@backstage/catalog-model';
+import {
+ Entity,
+ EntityMeta,
+ Location,
+ LOCATION_ANNOTATION,
+} from '@backstage/catalog-model';
import Knex from 'knex';
import lodash from 'lodash';
import { v4 as uuidv4 } from 'uuid';
@@ -167,7 +172,7 @@ export class CommonDatabase implements Database {
annotations: {
...(newEntity.metadata?.annotations ?? {}),
...(request.locationId
- ? { 'backstage.io/managed-by-location': request.locationId }
+ ? { [LOCATION_ANNOTATION]: request.locationId }
: {}),
},
};
diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts
index e9051d1f78..3300df1640 100644
--- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts
+++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts
@@ -15,7 +15,12 @@
*/
import { InputError } from '@backstage/backend-common';
-import { Entity, Location, LocationSpec } from '@backstage/catalog-model';
+import {
+ Entity,
+ Location,
+ LocationSpec,
+ LOCATION_ANNOTATION,
+} from '@backstage/catalog-model';
import lodash from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
@@ -23,8 +28,6 @@ import { IngestionModel } from '../ingestion';
import { AddLocationResult, HigherOrderOperation } from './types';
import { Logger } from 'winston';
-const LOCATION_ANNOTATION = 'backstage.io/managed-by-location';
-
/**
* Placeholder for operations that span several catalogs and/or stretches out
* in time.
diff --git a/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts b/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts
index 1f0f6ed539..56120fab23 100644
--- a/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts
+++ b/plugins/catalog-backend/src/ingestion/source/readers/GitHubLocationReader.test.ts
@@ -81,6 +81,8 @@ describe('Integration: GitHubLocationSource', () => {
});
it('fetches the fixture from backstage repo', async () => {
+ (fetch as any).mockResolvedValueOnce(new Response('component3'));
+
const PERMANENT_LINK =
'https://github.com/spotify/backstage/blob/ee84a874f8e37f87940cbe515a86c07a2db29541/plugins/catalog-backend/fixtures/one_component.yaml';
diff --git a/plugins/catalog-backend/src/run.ts b/plugins/catalog-backend/src/run.ts
index f4845168aa..b96989e4b8 100644
--- a/plugins/catalog-backend/src/run.ts
+++ b/plugins/catalog-backend/src/run.ts
@@ -18,7 +18,7 @@ import { getRootLogger } from '@backstage/backend-common';
import yn from 'yn';
import { startStandaloneServer } from './service/standaloneServer';
-const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003;
+const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
diff --git a/plugins/catalog-backend/src/service/standaloneApplication.ts b/plugins/catalog-backend/src/service/standaloneApplication.ts
index bdf296d06b..e8c1a226f0 100644
--- a/plugins/catalog-backend/src/service/standaloneApplication.ts
+++ b/plugins/catalog-backend/src/service/standaloneApplication.ts
@@ -56,7 +56,7 @@ export async function createStandaloneApplication(
app.use(express.json());
app.use(requestLoggingHandler());
app.use(
- '/',
+ '/catalog',
await createRouter({
entitiesCatalog,
locationsCatalog,
diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts
index 4ae4676703..2f9ffe302c 100644
--- a/plugins/catalog/src/api/CatalogClient.ts
+++ b/plugins/catalog/src/api/CatalogClient.ts
@@ -16,7 +16,11 @@
import { CatalogApi } from './types';
import { DescriptorEnvelope } from '../types';
-import { Entity, Location } from '@backstage/catalog-model';
+import {
+ Entity,
+ Location,
+ LOCATION_ANNOTATION,
+} from '@backstage/catalog-model';
export class CatalogClient implements CatalogApi {
private apiOrigin: string;
@@ -31,6 +35,22 @@ export class CatalogClient implements CatalogApi {
this.apiOrigin = apiOrigin;
this.basePath = basePath;
}
+ async getLocationById(id: String): Promise {
+ const response = await fetch(
+ `${this.apiOrigin}${this.basePath}/locations/${id}`,
+ );
+ if (response.ok) {
+ const location = await response.json();
+ if (location) return location.data;
+ }
+ return undefined;
+ }
+ async getEntitiesByLocationId(id: string): Promise {
+ const response = await fetch(
+ `${this.apiOrigin}${this.basePath}/entities?${LOCATION_ANNOTATION}=${id}`,
+ );
+ return await response.json();
+ }
async getEntities(): Promise {
const response = await fetch(`${this.apiOrigin}${this.basePath}/entities`);
return await response.json();
@@ -72,20 +92,11 @@ export class CatalogClient implements CatalogApi {
}
async getLocationByEntity(entity: Entity): Promise {
- const findLocationIdInEntity = (e: Entity): string | undefined =>
- e.metadata.annotations?.['backstage.io/managed-by-location'];
-
- const locationId = findLocationIdInEntity(entity);
+ const locationId = entity.metadata.annotations?.[LOCATION_ANNOTATION];
if (!locationId) return undefined;
- const response = await fetch(
- `${this.apiOrigin}${this.basePath}/locations/${locationId}`,
- );
- if (response.ok) {
- const location = await response.json();
- if (location) return location.data;
- }
+ const location = this.getLocationById(locationId);
- return undefined;
+ return location;
}
}
diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts
index 5bb6a6ca83..7d6c1d2b47 100644
--- a/plugins/catalog/src/api/types.ts
+++ b/plugins/catalog/src/api/types.ts
@@ -23,8 +23,10 @@ export const catalogApiRef = createApiRef({
});
export interface CatalogApi {
+ getLocationById(id: String): Promise;
getEntities(): Promise;
getEntityByName(name: string): Promise;
+ getEntitiesByLocationId(id: string): Promise;
addLocation(type: string, target: string): Promise;
getLocationByEntity(entity: Entity): Promise;
}
diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
index 779bcc7693..aeaef3e202 100644
--- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
+++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import React, { FC, useCallback, useState, useEffect } from 'react';
+import React, { FC, useCallback, useState } from 'react';
import {
Content,
ContentHeader,
@@ -25,8 +25,9 @@ import {
Page,
pageTheme,
useApi,
+ HeaderTabs,
} from '@backstage/core';
-import { useAsync, useMountedState } from 'react-use';
+import { useAsync } from 'react-use';
import CatalogTable from '../CatalogTable/CatalogTable';
import {
CatalogFilter,
@@ -37,7 +38,11 @@ import { filterGroups, defaultFilter } from '../../data/filters';
import { Link as RouterLink } from 'react-router-dom';
import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder';
import GitHub from '@material-ui/icons/GitHub';
-import { Entity, Location } from '@backstage/catalog-model';
+import {
+ Entity,
+ Location,
+ LOCATION_ANNOTATION,
+} from '@backstage/catalog-model';
const useStyles = makeStyles(theme => ({
contentWrapper: {
@@ -49,17 +54,15 @@ const useStyles = makeStyles(theme => ({
}));
import { catalogApiRef } from '../..';
-import { envelopeToComponent } from '../../data/utils';
+import { entityToComponent, findLocationForEntity } from '../../data/utils';
import { Component } from '../../data/component';
const CatalogPage: FC<{}> = () => {
const catalogApi = useApi(catalogApiRef);
const { value, error, loading } = useAsync(() => catalogApi.getEntities());
- const [locations, setLocations] = useState([]);
const [selectedFilter, setSelectedFilter] = useState(
defaultFilter,
);
- const isMounted = useMountedState();
const onFilterSelected = useCallback(
selected => setSelectedFilter(selected),
@@ -67,25 +70,26 @@ const CatalogPage: FC<{}> = () => {
);
const styles = useStyles();
- useEffect(() => {
+ const { value: locations } = useAsync(async () => {
const getLocationDataForEntities = async (entities: Entity[]) => {
return Promise.all(
- entities.map(entity => catalogApi.getLocationByEntity(entity)),
+ entities.map(entity => {
+ const locationId = entity.metadata.annotations?.[LOCATION_ANNOTATION];
+ if (!locationId) return undefined;
+
+ return catalogApi.getLocationById(locationId);
+ }),
);
};
if (value) {
- getLocationDataForEntities(value)
- .then(
- (location): Location[] =>
- location.filter(l => !!l) as Array,
- )
- .then(location => {
- if (isMounted()) setLocations(location);
- });
+ return getLocationDataForEntities(value).then(
+ (location): Location[] =>
+ location.filter(loc => !!loc) as Array,
+ );
}
- }, [value, catalogApi, isMounted]);
-
+ return [];
+ }, [value, catalogApi, catalogApi]);
const actions = [
(rowData: Component) => ({
icon: GitHub,
@@ -99,20 +103,32 @@ const CatalogPage: FC<{}> = () => {
}),
];
- const findLocationForEntity = (
- entity: Entity,
- l: Location[],
- ): Location | undefined => {
- const entityLocationId =
- entity.metadata.annotations?.['backstage.io/managed-by-location'];
- return l.find(location => location.id === entityLocationId);
- };
+ // TODO: replace me with the proper tabs implemntation
+ const tabs = [
+ {
+ id: 'services',
+ label: 'Services',
+ },
+ {
+ id: 'websites',
+ label: 'Websites',
+ },
+ {
+ id: 'libs',
+ label: 'Libraries',
+ },
+ {
+ id: 'documentation',
+ label: 'Documentation',
+ },
+ ];
return (
+
= () => {
}
/>
+
= () => {
onSelectedChange={onFilterSelected}
/>
-
- envelopeToComponent(
- val,
- findLocationForEntity(val, locations),
- ),
- )) ||
- []
- }
- loading={loading}
- error={error}
- actions={actions}
- />
+ {locations && (
+ {
+ return {
+ ...entityToComponent(val),
+ location: findLocationForEntity(val, locations),
+ };
+ })) ||
+ []
+ }
+ loading={loading}
+ error={error}
+ actions={actions}
+ />
+ )}
diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx
index 62f34f1620..b9c89604b0 100644
--- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx
+++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx
@@ -24,13 +24,16 @@ import {
useApi,
ErrorApi,
errorApiRef,
+ HeaderTabs,
} from '@backstage/core';
import ComponentContextMenu from '../ComponentContextMenu/ComponentContextMenu';
import ComponentRemovalDialog from '../ComponentRemovalDialog/ComponentRemovalDialog';
+
import { SentryIssuesWidget } from '@backstage/plugin-sentry';
import { Grid } from '@material-ui/core';
import { catalogApiRef } from '../..';
-import { envelopeToComponent } from '../../data/utils';
+import { entityToComponent } from '../../data/utils';
+import { Component } from '../../data/component';
const REDIRECT_DELAY = 1000;
@@ -54,18 +57,20 @@ const ComponentPage: FC = ({ match, history }) => {
const errorApi = useApi(errorApiRef);
const catalogApi = useApi(catalogApiRef);
- const catalogRequest = useAsync(() =>
- catalogApi.getEntityByName(match.params.name),
- );
+ const { value: component, error, loading } = useAsync(async () => {
+ const entity = await catalogApi.getEntityByName(match.params.name);
+ const location = await catalogApi.getLocationByEntity(entity);
+ return { ...entityToComponent(entity), location };
+ });
useEffect(() => {
- if (catalogRequest.error) {
+ if (error) {
errorApi.post(new Error('Component not found!'));
setTimeout(() => {
- history.push('/catalog');
+ history.push('/');
}, REDIRECT_DELAY);
}
- }, [catalogRequest.error, errorApi, history]);
+ }, [error, errorApi, history]);
if (componentName === '') {
history.push('/catalog');
@@ -76,17 +81,47 @@ const ComponentPage: FC = ({ match, history }) => {
setConfirmationDialogOpen(false);
setRemovingPending(true);
// await componentFactory.removeComponentByName(componentName);
- history.push('/catalog');
+
+ await catalogApi;
+ history.push('/');
};
- const component = envelopeToComponent(catalogRequest.value! ?? {});
+ // TODO - Replace with proper tabs implementation
+ const tabs = [
+ {
+ id: 'overview',
+ label: 'Overview',
+ },
+ {
+ id: 'ci',
+ label: 'CI/CD',
+ },
+ {
+ id: 'tests',
+ label: 'Tests',
+ },
+ {
+ id: 'api',
+ label: 'API',
+ },
+ {
+ id: 'monitoring',
+ label: 'Monitoring',
+ },
+ {
+ id: 'quality',
+ label: 'Quality',
+ },
+ ];
return (
-
+
- {confirmationDialogOpen && catalogRequest.value && (
+
+
+ {confirmationDialogOpen && component && (
= ({ match, history }) => {
diff --git a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx
index b5a863cef8..7dde95c8f0 100644
--- a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx
+++ b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx
@@ -25,6 +25,10 @@ import {
useTheme,
} from '@material-ui/core';
import { Component } from '../../data/component';
+import { useAsync } from 'react-use';
+import { useApi } from '@backstage/core';
+import { catalogApiRef } from '../../api/types';
+import { Entity } from '@backstage/catalog-model';
type ComponentRemovalDialogProps = {
onConfirm: () => any;
@@ -38,18 +42,28 @@ const ComponentRemovalDialog: FC = ({
onClose,
component,
}) => {
+ const catalogApi = useApi(catalogApiRef);
+ const { value } = useAsync(async () => {
+ let colocatedEntities: Array = [];
+ const locationId = component.location?.id;
+ if (locationId) {
+ colocatedEntities = await catalogApi.getEntitiesByLocationId(locationId);
+ }
+ return colocatedEntities;
+ });
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
+ const infoMessage = `This action will unregister ${
+ value ? value.map(e => e.metadata.name).join(', ') : ''
+ } from location with target ${component.location?.target}. To undo,
+ just re-register the component in Backstage.`;
return (
Are you sure you want to unregister this component?
-
- This action will unregister {component.name}. To undo, just
- re-register the component in Backstage.
-
+ {infoMessage}
diff --git a/plugins/catalog/src/data/utils.tsx b/plugins/catalog/src/data/utils.tsx
index 3f04883e34..93d28f5ca2 100644
--- a/plugins/catalog/src/data/utils.tsx
+++ b/plugins/catalog/src/data/utils.tsx
@@ -15,7 +15,11 @@
*/
import React from 'react';
import { Component } from './component';
-import { Entity, Location } from '@backstage/catalog-model';
+import {
+ Entity,
+ Location,
+ LOCATION_ANNOTATION,
+} from '@backstage/catalog-model';
import Edit from '@material-ui/icons/Edit';
import IconButton from '@material-ui/core/IconButton';
import { styled } from '@material-ui/core/styles';
@@ -34,7 +38,7 @@ const createEditLink = (location: Location): string => {
}
};
-export function envelopeToComponent(
+export function entityToComponent(
envelope: Entity,
location?: Location,
): Component {
@@ -56,3 +60,15 @@ export function envelopeToComponent(
location,
};
}
+
+export function findLocationForEntity(
+ entity: Entity,
+ locations: Location[],
+): Location | undefined {
+ for (const loc of locations) {
+ if (loc.id === entity.metadata.annotations?.[LOCATION_ANNOTATION]) {
+ return loc;
+ }
+ }
+ return undefined;
+}
diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx
index ad47c3a41b..ca463550d1 100644
--- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx
+++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx
@@ -30,6 +30,8 @@ const catalogApi: jest.Mocked = {
getEntities: jest.fn(),
getEntityByName: jest.fn(),
getLocationByEntity: jest.fn(),
+ getLocationById: jest.fn(),
+ getEntitiesByLocationId: jest.fn(),
};
const setup = () => ({
diff --git a/plugins/tech-radar/src/api.ts b/plugins/tech-radar/src/api.ts
index 2e068fe49a..8a78da8027 100644
--- a/plugins/tech-radar/src/api.ts
+++ b/plugins/tech-radar/src/api.ts
@@ -35,8 +35,8 @@ export interface RadarEntry {
key: string; // react key
id: string;
moved: number;
- quadrant: string;
- ring: string;
+ quadrant: RadarQuadrant;
+ ring: RadarRing;
title: string;
url: string;
}
diff --git a/plugins/tech-radar/src/components/Radar/Radar.jsx b/plugins/tech-radar/src/components/Radar/Radar.jsx
deleted file mode 100644
index 11f763f465..0000000000
--- a/plugins/tech-radar/src/components/Radar/Radar.jsx
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * 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 PropTypes from 'prop-types';
-import { forceCollide, forceSimulation } from 'd3-force';
-import RadarPlot from '../RadarPlot';
-import Segment from '../../utils/segment';
-import color from 'color';
-
-export default class Radar extends React.Component {
- static adjustQuadrants(quadrants, radius, width, height) {
- /*
- 0 1 2 3 ← x stops index
- │ │ │ │ ↓ y stops index
- ┼───────────┼─────────────────────────────┼───────────┼─0
- │ │ . -- ~~~ -- . │ │
- │ │ .-~ ~-. │ │
- │ │ / \ │ │
- │ │ / \ │ │
- ┼───────────┼─────────────────────────────┼───────────┼─1
- ┼───────────┼─────────────────────────────┼───────────┼─2
- │ │ | | │ │
- │ │ \ / │ │
- │ │ \ / │ │
- │ │ `-. .-' │ │
- │ │ ~- . ___ . -~ │ │
- ┼───────────┼─────────────────────────────┼───────────┼─3
- */
- const margin = 16;
- const xStops = [
- margin,
- width / 2 - radius - margin,
- width / 2 + radius + margin,
- width - margin,
- ];
- const yStops = [margin, height / 2 - margin, height / 2, height - margin];
-
- // The quadrant parameters correspond to Q[0..3] above. They are in this order because of the
- // original Zalando code; maybe we should refactor them to be in reverse order?
- const legendParams = [
- {
- x: xStops[2],
- y: yStops[2],
- width: xStops[3] - xStops[2],
- height: yStops[3] - yStops[2],
- },
- {
- x: xStops[0],
- y: yStops[2],
- width: xStops[1] - xStops[0],
- height: yStops[3] - yStops[2],
- },
- {
- x: xStops[0],
- y: yStops[0],
- width: xStops[1] - xStops[0],
- height: yStops[1] - yStops[0],
- },
- {
- x: xStops[2],
- y: yStops[0],
- width: xStops[3] - xStops[2],
- height: yStops[1] - yStops[0],
- },
- ];
-
- quadrants.forEach((quadrant, idx) => {
- const legendParam = legendParams[idx % 4];
-
- quadrant.idx = idx;
- quadrant.radialMin = (idx * Math.PI) / 2;
- quadrant.radialMax = ((idx + 1) * Math.PI) / 2;
- quadrant.offsetX = idx % 4 === 0 || idx % 4 === 3 ? 1 : -1;
- quadrant.offsetY = idx % 4 === 0 || idx % 4 === 1 ? 1 : -1;
- quadrant.legendX = legendParam.x;
- quadrant.legendY = legendParam.y;
- quadrant.legendWidth = legendParam.width;
- quadrant.legendHeight = legendParam.height;
- });
- }
-
- static adjustRings(rings, radius) {
- rings.forEach((ring, idx) => {
- ring.idx = idx;
- ring.outerRadius = ((idx + 2) / (rings.length + 1)) * radius;
- ring.innerRadius =
- ((idx === 0 ? 0 : idx + 1) / (rings.length + 1)) * radius;
- });
- }
-
- static adjustEntries(entries, activeEntry, quadrants, rings, radius) {
- let seed = 42;
- entries.forEach((entry, idx) => {
- const quadrant = quadrants.find(q => {
- const match =
- typeof entry.quadrant === 'object'
- ? entry.quadrant.id
- : entry.quadrant;
- return q.id === match;
- });
- const ring = rings.find(r => {
- const match =
- typeof entry.ring === 'object' ? entry.ring.id : entry.ring;
- return r.id === match;
- });
-
- if (!quadrant) {
- throw new Error(
- `Unknown quadrant ${entry.quadrant} for entry ${entry.id}!`,
- );
- }
- if (!ring) {
- throw new Error(`Unknown ring ${entry.ring} for entry ${entry.id}!`);
- }
-
- entry.idx = idx;
- entry.quadrant = quadrant;
- entry.ring = ring;
- entry.segment = new Segment(quadrant, ring, radius, () => seed++);
- const point = entry.segment.random();
- entry.x = point.x;
- entry.y = point.y;
- entry.active = activeEntry ? entry.id === activeEntry.id : false;
- entry.color = entry.active
- ? entry.ring.color
- : color(entry.ring.color).desaturate(0.5).lighten(0.1).string();
- });
-
- const simulation = forceSimulation()
- .nodes(entries)
- .velocityDecay(0.19)
- .force('collision', forceCollide().radius(12).strength(0.85))
- .stop();
-
- for (
- let i = 0,
- n = Math.ceil(
- Math.log(simulation.alphaMin()) /
- Math.log(1 - simulation.alphaDecay()),
- );
- i < n;
- ++i
- ) {
- simulation.tick();
-
- for (const entry of entries) {
- entry.x = entry.segment.clipx(entry);
- entry.y = entry.segment.clipy(entry);
- }
- }
- }
-
- constructor(props) {
- super(props);
- this.state = { activeEntry: null };
- }
-
- _setActiveEntry(entry) {
- this.setState({ activeEntry: entry });
- }
-
- _clearActiveEntry() {
- this.setState({ activeEntry: null });
- }
-
- render() {
- // TODO(dflemstr): most of this method can be heavily memoized if performance becomes a problem
-
- const { width, height, quadrants, rings, entries } = this.props;
- const { activeEntry } = this.state;
- const radius = Math.min(width, height) / 2;
-
- Radar.adjustQuadrants(quadrants, radius, width, height);
- Radar.adjustRings(rings, radius);
- Radar.adjustEntries(entries, activeEntry, quadrants, rings, radius);
-
- return (
- {
- this.node = node;
- }}
- width={width}
- height={height}
- {...this.props.svgProps}
- >
- this._setActiveEntry(entry)}
- onEntryMouseLeave={() => this._clearActiveEntry()}
- />
-
- );
- }
-}
-
-Radar.propTypes = {
- width: PropTypes.number.isRequired,
- height: PropTypes.number.isRequired,
- quadrants: PropTypes.arrayOf(PropTypes.object).isRequired,
- rings: PropTypes.arrayOf(PropTypes.object).isRequired,
- entries: PropTypes.arrayOf(PropTypes.object).isRequired,
- svgProps: PropTypes.object,
-};
diff --git a/plugins/tech-radar/src/components/Radar/Radar.test.tsx b/plugins/tech-radar/src/components/Radar/Radar.test.tsx
index f68002ade2..469f0d85cf 100644
--- a/plugins/tech-radar/src/components/Radar/Radar.test.tsx
+++ b/plugins/tech-radar/src/components/Radar/Radar.test.tsx
@@ -31,9 +31,9 @@ const minProps = {
{
id: 'typescript',
title: 'TypeScript',
- quadrant: 'languages',
+ quadrant: { id: 'languages', name: 'Languages' },
moved: 0,
- ring: 'use',
+ ring: { id: 'use', name: 'USE', color: '#93c47d' },
url: '#',
},
],
diff --git a/plugins/tech-radar/src/components/Radar/Radar.tsx b/plugins/tech-radar/src/components/Radar/Radar.tsx
new file mode 100644
index 0000000000..1db655730d
--- /dev/null
+++ b/plugins/tech-radar/src/components/Radar/Radar.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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, { FC, useState, useRef } from 'react';
+import RadarPlot from '../RadarPlot';
+import { Ring, Quadrant, Entry } from '../../utils/types';
+import { adjustQuadrants, adjustRings, adjustEntries } from './utils';
+
+type Props = {
+ width: number;
+ height: number;
+ quadrants: Quadrant[];
+ rings: Ring[];
+ entries: Entry[];
+ svgProps?: object;
+};
+
+const Radar: FC = props => {
+ const { width, height, quadrants, rings, entries } = props;
+ const radius = Math.min(width, height) / 2;
+
+ const [activeEntry, setActiveEntry] = useState();
+ const node = useRef(null);
+
+ // TODO(dflemstr): most of this can be heavily memoized if performance becomes a problem
+ adjustQuadrants(quadrants, radius, width, height);
+ adjustRings(rings, radius);
+ adjustEntries(entries, activeEntry, quadrants, rings, radius);
+
+ return (
+
+ setActiveEntry(entry)}
+ onEntryMouseLeave={() => setActiveEntry(null)}
+ />
+
+ );
+};
+
+export default Radar;
diff --git a/plugins/tech-radar/src/components/Radar/utils.ts b/plugins/tech-radar/src/components/Radar/utils.ts
new file mode 100644
index 0000000000..a8896ddf31
--- /dev/null
+++ b/plugins/tech-radar/src/components/Radar/utils.ts
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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 color from 'color';
+import { forceCollide, forceSimulation } from 'd3-force';
+import Segment from '../../utils/segment';
+import { Ring, Quadrant, Entry } from '../../utils/types';
+
+export const adjustQuadrants = (
+ quadrants: Quadrant[],
+ radius: number,
+ width: number,
+ height: number,
+) => {
+ /*
+ 0 1 2 3 ← x stops index
+ │ │ │ │ ↓ y stops index
+ ┼───────────┼─────────────────────────────┼───────────┼─0
+ │ │ . -- ~~~ -- . │ │
+ │ │ .-~ ~-. │ │
+ │ │ / \ │ │
+ │ │ / \ │ │
+ ┼───────────┼─────────────────────────────┼───────────┼─1
+ ┼───────────┼─────────────────────────────┼───────────┼─2
+ │ │ | | │ │
+ │ │ \ / │ │
+ │ │ \ / │ │
+ │ │ `-. .-' │ │
+ │ │ ~- . ___ . -~ │ │
+ ┼───────────┼─────────────────────────────┼───────────┼─3
+ */
+
+ const margin = 16;
+ const xStops = [
+ margin,
+ width / 2 - radius - margin,
+ width / 2 + radius + margin,
+ width - margin,
+ ];
+ const yStops = [margin, height / 2 - margin, height / 2, height - margin];
+
+ // The quadrant parameters correspond to Q[0..3] above. They are in this order because of the
+ // original Zalando code; maybe we should refactor them to be in reverse order?
+ const legendParams = [
+ {
+ x: xStops[2],
+ y: yStops[2],
+ width: xStops[3] - xStops[2],
+ height: yStops[3] - yStops[2],
+ },
+ {
+ x: xStops[0],
+ y: yStops[2],
+ width: xStops[1] - xStops[0],
+ height: yStops[3] - yStops[2],
+ },
+ {
+ x: xStops[0],
+ y: yStops[0],
+ width: xStops[1] - xStops[0],
+ height: yStops[1] - yStops[0],
+ },
+ {
+ x: xStops[2],
+ y: yStops[0],
+ width: xStops[3] - xStops[2],
+ height: yStops[1] - yStops[0],
+ },
+ ];
+
+ quadrants.forEach((quadrant, idx) => {
+ const legendParam = legendParams[idx % 4];
+
+ quadrant.idx = idx;
+ quadrant.radialMin = (idx * Math.PI) / 2;
+ quadrant.radialMax = ((idx + 1) * Math.PI) / 2;
+ quadrant.offsetX = idx % 4 === 0 || idx % 4 === 3 ? 1 : -1;
+ quadrant.offsetY = idx % 4 === 0 || idx % 4 === 1 ? 1 : -1;
+ quadrant.legendX = legendParam.x;
+ quadrant.legendY = legendParam.y;
+ quadrant.legendWidth = legendParam.width;
+ quadrant.legendHeight = legendParam.height;
+ });
+};
+
+export const adjustEntries = (
+ entries: Entry[],
+ activeEntry: Entry | null | undefined,
+ quadrants: Quadrant[],
+ rings: Ring[],
+ radius: number,
+) => {
+ let seed = 42;
+ entries.forEach((entry, idx) => {
+ const quadrant = quadrants.find(q => {
+ const match =
+ typeof entry.quadrant === 'object' ? entry.quadrant.id : entry.quadrant;
+ return q.id === match;
+ });
+ const ring = rings.find(r => {
+ const match = typeof entry.ring === 'object' ? entry.ring.id : entry.ring;
+ return r.id === match;
+ });
+
+ if (!quadrant) {
+ throw new Error(
+ `Unknown quadrant ${entry.quadrant} for entry ${entry.id}!`,
+ );
+ }
+ if (!ring) {
+ throw new Error(`Unknown ring ${entry.ring} for entry ${entry.id}!`);
+ }
+
+ entry.idx = idx;
+ entry.quadrant = quadrant;
+ entry.ring = ring;
+ entry.segment = new Segment(quadrant, ring, radius, () => seed++);
+ const point = entry.segment.random();
+ entry.x = point.x;
+ entry.y = point.y;
+ entry.active = activeEntry ? entry.id === activeEntry.id : false;
+ entry.color = entry.active
+ ? entry.ring.color
+ : color(entry.ring.color).desaturate(0.5).lighten(0.1).string();
+ });
+
+ const simulation = forceSimulation()
+ .nodes(entries)
+ .velocityDecay(0.19)
+ .force('collision', forceCollide().radius(12).strength(0.85))
+ .stop();
+
+ for (
+ let i = 0,
+ n = Math.ceil(
+ Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()),
+ );
+ i < n;
+ ++i
+ ) {
+ simulation.tick();
+
+ for (const entry of entries) {
+ if (entry.segment) {
+ entry.x = entry.segment.clipx(entry);
+ entry.y = entry.segment.clipy(entry);
+ }
+ }
+ }
+};
+
+export const adjustRings = (rings: Ring[], radius: number) => {
+ rings.forEach((ring, idx) => {
+ ring.idx = idx;
+ ring.outerRadius = ((idx + 2) / (rings.length + 1)) * radius;
+ ring.innerRadius =
+ ((idx === 0 ? 0 : idx + 1) / (rings.length + 1)) * radius;
+ });
+};
diff --git a/plugins/tech-radar/src/components/RadarBubble/RadarBubble.jsx b/plugins/tech-radar/src/components/RadarBubble/RadarBubble.jsx
deleted file mode 100644
index 072f80ca20..0000000000
--- a/plugins/tech-radar/src/components/RadarBubble/RadarBubble.jsx
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * 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 PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core';
-
-const styles = {
- bubble: {
- pointerEvents: 'none',
- userSelect: 'none',
- opacity: 0,
- },
- visibleBubble: {
- pointerEvents: 'none',
- userSelect: 'none',
- opacity: 0.8,
- },
- background: {
- fill: '#333',
- },
- text: {
- pointerEvents: 'none',
- userSelect: 'none',
- fontSize: '10px',
- fill: '#fff',
- },
-};
-
-class RadarBubble extends React.PureComponent {
- componentDidMount() {
- this._updatePosition();
- }
-
- componentDidUpdate() {
- this._updatePosition();
- }
-
- _setRect = rect => {
- this.rect = rect;
- };
- _setNode = node => {
- this.node = node;
- };
- _setText = text => {
- this.text = text;
- };
- _setPath = path => {
- this.path = path;
- };
-
- _updatePosition() {
- // We can't do this in render() because we need to measure how big the text is to draw the bubble around it
- // this.text will not be set during testing because there is no real DOM
- if (this.text) {
- const { x, y } = this.props;
- const bbox = this.text.getBBox();
- const marginX = 5;
- const marginY = 4;
- this.node.setAttribute(
- 'transform',
- `translate(${x - bbox.width / 2}, ${y - bbox.height - marginY})`,
- );
- this.rect.setAttribute('x', -marginX);
- this.rect.setAttribute('y', -bbox.height);
- this.rect.setAttribute('width', bbox.width + 2 * marginX);
- this.rect.setAttribute('height', bbox.height + marginY);
- this.path.setAttribute(
- 'transform',
- `translate(${bbox.width / 2 - marginX}, ${marginY - 1})`,
- );
- }
- }
-
- render() {
- const { visible, text, classes } = this.props;
- return (
-
-
-
- {text}
-
-
-
- );
- }
-}
-
-RadarBubble.propTypes = {
- visible: PropTypes.bool.isRequired,
- text: PropTypes.string.isRequired,
- x: PropTypes.number.isRequired,
- y: PropTypes.number.isRequired,
- classes: PropTypes.object.isRequired,
-};
-
-export default withStyles(styles)(RadarBubble);
diff --git a/plugins/tech-radar/src/components/RadarBubble/RadarBubble.tsx b/plugins/tech-radar/src/components/RadarBubble/RadarBubble.tsx
new file mode 100644
index 0000000000..28890f984b
--- /dev/null
+++ b/plugins/tech-radar/src/components/RadarBubble/RadarBubble.tsx
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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, { FC, useRef, useEffect, useLayoutEffect } from 'react';
+import { makeStyles, Theme } from '@material-ui/core';
+
+type Props = {
+ visible: boolean;
+ text: string;
+ x: number;
+ y: number;
+};
+
+const useStyles = makeStyles(() => ({
+ bubble: {
+ pointerEvents: 'none',
+ userSelect: 'none',
+ opacity: 0,
+ },
+ visibleBubble: {
+ pointerEvents: 'none',
+ userSelect: 'none',
+ opacity: 0.8,
+ },
+ background: {
+ fill: '#333',
+ },
+ text: {
+ pointerEvents: 'none',
+ userSelect: 'none',
+ fontSize: '10px',
+ fill: '#fff',
+ },
+}));
+
+const RadarBubble: FC = props => {
+ const classes = useStyles(props);
+ const { visible, text } = props;
+
+ const textElem = useRef(null);
+ const svgElem = useRef(null);
+ const rectElem = useRef(null);
+ const pathElem = useRef(null);
+
+ const updatePosition = () => {
+ if (textElem.current) {
+ const { x, y } = props;
+ const bbox = textElem.current.getBBox();
+ const marginX = 5;
+ const marginY = 4;
+
+ if (svgElem.current) {
+ svgElem.current.setAttribute(
+ 'transform',
+ `translate(${x - bbox.width / 2}, ${y - bbox.height - marginY})`,
+ );
+ }
+
+ if (rectElem.current) {
+ rectElem.current.setAttribute('x', String(-marginX));
+ rectElem.current.setAttribute('y', String(-bbox.height));
+ rectElem.current.setAttribute(
+ 'width',
+ String(bbox.width + 2 * marginX),
+ );
+ rectElem.current.setAttribute('height', String(bbox.height + marginY));
+ }
+
+ if (pathElem.current) {
+ pathElem.current.setAttribute(
+ 'transform',
+ `translate(${bbox.width / 2 - marginX}, ${marginY - 1})`,
+ );
+ }
+ }
+ };
+
+ useEffect(() => {
+ updatePosition();
+ }, []);
+
+ useLayoutEffect(() => {
+ updatePosition();
+ });
+
+ return (
+
+
+
+ {text}
+
+
+
+ );
+};
+
+export default RadarBubble;
diff --git a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.jsx b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.jsx
deleted file mode 100644
index a6dc9e1fa0..0000000000
--- a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.jsx
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * 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 PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core';
-
-const styles = {
- text: {
- pointerEvents: 'none',
- userSelect: 'none',
- fontSize: '9px',
- fill: '#fff',
- textAnchor: 'middle',
- },
-
- link: {
- cursor: 'pointer',
- },
-};
-
-class RadarEntry extends React.PureComponent {
- render() {
- const {
- moved,
- color,
- url,
- number,
- x,
- y,
- onMouseEnter,
- onMouseLeave,
- onClick,
- classes,
- } = this.props;
-
- const style = { fill: color };
-
- let blip;
- if (moved > 0) {
- blip = ; // triangle pointing up
- } else if (moved < 0) {
- blip = ; // triangle pointing down
- } else {
- blip = ;
- }
-
- if (url) {
- blip = (
-
- {blip}
-
- );
- }
-
- return (
-
- {blip}
-
- {number}
-
-
- );
- }
-}
-
-RadarEntry.propTypes = {
- x: PropTypes.number.isRequired,
- y: PropTypes.number.isRequired,
- number: PropTypes.number.isRequired,
- color: PropTypes.string.isRequired,
- url: PropTypes.string,
- moved: PropTypes.number,
- onMouseEnter: PropTypes.func,
- onMouseLeave: PropTypes.func,
- onClick: PropTypes.func,
- classes: PropTypes.object.isRequired,
-};
-
-export default withStyles(styles)(RadarEntry);
diff --git a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx
new file mode 100644
index 0000000000..6006979e67
--- /dev/null
+++ b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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, { FC } from 'react';
+import { makeStyles, Theme } from '@material-ui/core';
+
+type Props = {
+ x: number;
+ y: number;
+ number: number;
+ color: string;
+ url?: string;
+ moved?: number;
+ onMouseEnter?: (event: React.MouseEvent) => void;
+ onMouseLeave?: (event: React.MouseEvent) => void;
+ onClick?: (event: React.MouseEvent) => void;
+};
+
+const useStyles = makeStyles(() => ({
+ text: {
+ pointerEvents: 'none',
+ userSelect: 'none',
+ fontSize: '9px',
+ fill: '#fff',
+ textAnchor: 'middle',
+ },
+
+ link: {
+ cursor: 'pointer',
+ },
+}));
+
+const RadarEntry: FC = props => {
+ const classes = useStyles(props);
+
+ const {
+ moved,
+ color,
+ url,
+ number,
+ x,
+ y,
+ onMouseEnter,
+ onMouseLeave,
+ onClick,
+ } = props;
+
+ const style = { fill: color };
+
+ let blip;
+ if (moved && moved > 0) {
+ blip = ; // triangle pointing up
+ } else if (moved && moved < 0) {
+ blip = ; // triangle pointing down
+ } else {
+ blip = ;
+ }
+
+ if (url) {
+ blip = (
+
+ {blip}
+
+ );
+ }
+
+ return (
+
+ {blip}
+
+ {number}
+
+
+ );
+};
+
+export default RadarEntry;
diff --git a/plugins/tech-radar/src/components/RadarFooter/RadarFooter.jsx b/plugins/tech-radar/src/components/RadarFooter/RadarFooter.tsx
similarity index 52%
rename from plugins/tech-radar/src/components/RadarFooter/RadarFooter.jsx
rename to plugins/tech-radar/src/components/RadarFooter/RadarFooter.tsx
index c30079e5bf..7d60a63f59 100644
--- a/plugins/tech-radar/src/components/RadarFooter/RadarFooter.jsx
+++ b/plugins/tech-radar/src/components/RadarFooter/RadarFooter.tsx
@@ -14,39 +14,32 @@
* limitations under the License.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core';
+import React, { FC } from 'react';
+import { makeStyles, Theme } from '@material-ui/core';
-const styles = {
+type Props = {
+ x: number;
+ y: number;
+};
+
+const useStyles = makeStyles(() => ({
text: {
pointerEvents: 'none',
userSelect: 'none',
fontSize: '10px',
fill: '#000',
},
+}));
+
+const RadarFooter: FC = props => {
+ const { x, y } = props;
+ const classes = useStyles(props);
+
+ return (
+
+ {'▲ moved up\u00a0\u00a0\u00a0\u00a0\u00a0▼ moved down'}
+
+ );
};
-class RadarFooter extends React.PureComponent {
- render() {
- const { x, y, classes } = this.props;
-
- return (
-
- {'▲ moved up\u00a0\u00a0\u00a0\u00a0\u00a0▼ moved down'}
-
- );
- }
-}
-
-RadarFooter.propTypes = {
- x: PropTypes.number.isRequired,
- y: PropTypes.number.isRequired,
- classes: PropTypes.object.isRequired,
-};
-
-export default withStyles(styles)(RadarFooter);
+export default RadarFooter;
diff --git a/plugins/tech-radar/src/components/RadarGrid/RadarGrid.jsx b/plugins/tech-radar/src/components/RadarGrid/RadarGrid.jsx
deleted file mode 100644
index ce6c046242..0000000000
--- a/plugins/tech-radar/src/components/RadarGrid/RadarGrid.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * 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 PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core';
-import * as CommonPropTypes from '../../utils/prop-types';
-
-const styles = {
- ring: {
- fill: 'none',
- stroke: '#bbb',
- strokeWidth: '1px',
- },
- axis: {
- fill: 'none',
- stroke: '#bbb',
- strokeWidth: '1px',
- },
- text: {
- pointerEvents: 'none',
- userSelect: 'none',
- fill: '#e5e5e5',
- fontSize: '25px',
- fontWeight: 800,
- },
-};
-
-// A component for the background grid of the radar, with axes, rings etc. It will render around the origin, i.e.
-// assume that (0, 0) is in the middle of the drawing.
-class RadarGrid extends React.PureComponent {
- render() {
- const { radius, rings, classes } = this.props;
-
- const makeRingNode = (ringRadius, ringIndex) => [
- ,
-
- {rings[ringIndex].name}
- ,
- ];
-
- const axisNodes = [
- // X axis
- ,
- // Y axis
- ,
- ];
-
- const ringNodes = rings.map(r => r.outerRadius).map(makeRingNode);
-
- return axisNodes.concat(ringNodes);
- }
-}
-
-RadarGrid.propTypes = {
- radius: PropTypes.number.isRequired,
- rings: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.RING)).isRequired,
- classes: PropTypes.object.isRequired, // this is the withStyles HOC
-};
-
-export default withStyles(styles)(RadarGrid);
diff --git a/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx b/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx
new file mode 100644
index 0000000000..514afbe958
--- /dev/null
+++ b/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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 { makeStyles, Theme } from '@material-ui/core';
+import { Ring } from '../../utils/types';
+
+type Props = {
+ radius: number;
+ rings: Ring[];
+};
+
+const useStyles = makeStyles(() => ({
+ ring: {
+ fill: 'none',
+ stroke: '#bbb',
+ strokeWidth: '1px',
+ },
+ axis: {
+ fill: 'none',
+ stroke: '#bbb',
+ strokeWidth: '1px',
+ },
+ text: {
+ pointerEvents: 'none',
+ userSelect: 'none',
+ fill: '#e5e5e5',
+ fontSize: '25px',
+ fontWeight: 800,
+ },
+}));
+
+// A component for the background grid of the radar, with axes, rings etc. It will render around the origin, i.e.
+// assume that (0, 0) is in the middle of the drawing.
+const RadarGrid = (props: Props) => {
+ const { radius, rings } = props;
+ const classes = useStyles(props);
+
+ const makeRingNode = (ringRadius: number | undefined, ringIndex: number) => [
+ ,
+
+ {rings[ringIndex].name}
+ ,
+ ];
+
+ const axisNodes = [
+ // X axis
+ ,
+ // Y axis
+ ,
+ ];
+
+ const ringNodes = rings.map(r => r.outerRadius).map(makeRingNode);
+
+ return <>{axisNodes.concat(...ringNodes)}>;
+};
+
+export default RadarGrid;
diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.jsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx
similarity index 59%
rename from plugins/tech-radar/src/components/RadarLegend/RadarLegend.jsx
rename to plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx
index 674dd34cbb..03146a8071 100644
--- a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.jsx
+++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx
@@ -13,12 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withStyles } from '@material-ui/core';
-import * as CommonPropTypes from '../../utils/prop-types';
+import React, { FC } from 'react';
+import { makeStyles, Theme } from '@material-ui/core';
+import { Quadrant, Ring, Entry } from '../../utils/types';
-const styles = {
+type Segments = {
+ [k: number]: { [k: number]: Entry[] };
+};
+
+type Props = {
+ quadrants: Quadrant[];
+ rings: Ring[];
+ entries: Entry[];
+ onEntryMouseEnter?: (entry: Entry) => void;
+ onEntryMouseLeave?: (entry: Entry) => void;
+};
+
+const useStyles = makeStyles(() => ({
quadrant: {
height: '100%',
width: '100%',
@@ -66,50 +77,29 @@ const styles = {
entryLink: {
pointerEvents: 'none',
},
-};
+}));
-class RadarLegend extends React.PureComponent {
- static _renderQuadrant(
- segments,
- quadrant,
- rings,
- onEntryMouseEnter,
- onEntryMouseLeave,
- classes,
- ) {
- return (
-
-
-
{quadrant.name}
-
- {rings.map(ring =>
- RadarLegend._renderRing(
- ring,
- RadarLegend._getSegment(segments, quadrant, ring),
- onEntryMouseEnter,
- onEntryMouseLeave,
- classes,
- ),
- )}
-
-
-
- );
- }
+const RadarLegend: FC = props => {
+ const classes = useStyles(props);
- static _renderRing(
- ring,
- entries,
- onEntryMouseEnter,
- onEntryMouseLeave,
- classes,
- ) {
+ const _getSegment = (
+ segmented: Segments,
+ quadrant: Quadrant,
+ ring: Ring,
+ ringOffset = 0,
+ ) => {
+ const qidx = quadrant.idx;
+ const ridx = ring.idx;
+ const segmentedData = qidx === undefined ? {} : segmented[qidx] || {};
+ return ridx === undefined ? [] : segmentedData[ridx + ringOffset] || [];
+ };
+
+ const _renderRing = (
+ ring: Ring,
+ entries: Entry[],
+ onEntryMouseEnter?: Props['onEntryMouseEnter'],
+ onEntryMouseLeave?: Props['onEntryMouseEnter'],
+ ) => {
return (
{ring.name}
@@ -131,12 +121,12 @@ class RadarLegend extends React.PureComponent {
return (
onEntryMouseEnter(entry))
}
onMouseLeave={
- onEntryMouseEnter && (() => onEntryMouseLeave(entry))
+ onEntryMouseLeave && (() => onEntryMouseLeave(entry))
}
>
{node}
@@ -147,57 +137,93 @@ class RadarLegend extends React.PureComponent {
)}
);
- }
+ };
- static _getSegment(segmented, quadrant, ring, ringOffset = 0) {
- return (segmented[quadrant.idx] || {})[ring.idx + ringOffset] || [];
- }
+ const _renderQuadrant = (
+ segments: Segments,
+ quadrant: Quadrant,
+ rings: Ring[],
+ onEntryMouseEnter: Props['onEntryMouseEnter'],
+ onEntryMouseLeave: Props['onEntryMouseLeave'],
+ ) => {
+ return (
+
+
+
{quadrant.name}
+
+ {rings.map(ring =>
+ _renderRing(
+ ring,
+ _getSegment(segments, quadrant, ring),
+ onEntryMouseEnter,
+ onEntryMouseLeave,
+ ),
+ )}
+
+
+
+ );
+ };
- render() {
- const {
- quadrants,
- rings,
- entries,
- onEntryMouseEnter,
- onEntryMouseLeave,
- classes,
- } = this.props;
-
- const segments = {};
+ const _setupSegments = (entries: Entry[]) => {
+ const segments: Segments = {};
for (const entry of entries) {
const qidx = entry.quadrant.idx;
const ridx = entry.ring.idx;
- const quadrantData = segments[qidx] || (segments[qidx] = {});
- const ringData = quadrantData[ridx] || (quadrantData[ridx] = []);
+ let quadrantData: { [k: number]: Entry[] } = {};
+ if (qidx !== undefined) {
+ if (segments[qidx] === undefined) {
+ segments[qidx] = {};
+ }
+
+ quadrantData = segments[qidx];
+ }
+
+ let ringData = [];
+ if (ridx !== undefined) {
+ if (quadrantData[ridx] === undefined) {
+ quadrantData[ridx] = [];
+ }
+
+ ringData = quadrantData[ridx];
+ }
+
ringData.push(entry);
}
- return (
-
- {quadrants.map(quadrant =>
- RadarLegend._renderQuadrant(
- segments,
- quadrant,
- rings,
- onEntryMouseEnter,
- onEntryMouseLeave,
- classes,
- ),
- )}
-
- );
- }
-}
+ return segments;
+ };
-RadarLegend.propTypes = {
- quadrants: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.QUADRANT))
- .isRequired,
- rings: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.RING)).isRequired,
- entries: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.ENTRY)).isRequired,
- onEntryMouseEnter: PropTypes.func,
- onEntryMouseLeave: PropTypes.func,
- classes: PropTypes.object.isRequired,
+ const {
+ quadrants,
+ rings,
+ entries,
+ onEntryMouseEnter,
+ onEntryMouseLeave,
+ } = props;
+
+ const segments: Segments = _setupSegments(entries);
+
+ return (
+
+ {quadrants.map(quadrant =>
+ _renderQuadrant(
+ segments,
+ quadrant,
+ rings,
+ onEntryMouseEnter,
+ onEntryMouseLeave,
+ ),
+ )}
+
+ );
};
-export default withStyles(styles)(RadarLegend);
+export default RadarLegend;
diff --git a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.jsx b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.jsx
deleted file mode 100644
index b93d20cbff..0000000000
--- a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.jsx
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * 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 PropTypes from 'prop-types';
-import * as CommonPropTypes from '../../utils/prop-types';
-
-import RadarGrid from '../RadarGrid';
-import RadarEntry from '../RadarEntry';
-import RadarBubble from '../RadarBubble';
-import RadarFooter from '../RadarFooter';
-import RadarLegend from '../RadarLegend';
-
-// A component that draws the radar circle.
-export default class RadarPlot extends React.PureComponent {
- render() {
- const {
- width,
- height,
- radius,
- quadrants,
- rings,
- entries,
- activeEntry,
- onEntryMouseEnter,
- onEntryMouseLeave,
- } = this.props;
-
- return (
-
- onEntryMouseEnter(entry))
- }
- onEntryMouseLeave={
- onEntryMouseLeave && (entry => onEntryMouseLeave(entry))
- }
- />
-
-
-
- {entries.map(entry => (
- onEntryMouseEnter(entry))
- }
- onMouseLeave={
- onEntryMouseLeave && (() => onEntryMouseLeave(entry))
- }
- />
- ))}
-
-
-
- );
- }
-}
-
-RadarPlot.propTypes = {
- width: PropTypes.number.isRequired,
- height: PropTypes.number.isRequired,
- radius: PropTypes.number.isRequired,
- rings: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.RING)).isRequired,
- quadrants: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.QUADRANT))
- .isRequired,
- entries: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.ENTRY)).isRequired,
- activeEntry: PropTypes.object,
- onEntryMouseEnter: PropTypes.func,
- onEntryMouseLeave: PropTypes.func,
-};
diff --git a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx
new file mode 100644
index 0000000000..48ad660dde
--- /dev/null
+++ b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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, { FC } from 'react';
+import { Quadrant, Ring, Entry } from '../../utils/types';
+
+import RadarGrid from '../RadarGrid';
+import RadarEntry from '../RadarEntry';
+import RadarBubble from '../RadarBubble';
+import RadarFooter from '../RadarFooter';
+import RadarLegend from '../RadarLegend';
+
+type Props = {
+ width: number;
+ height: number;
+ radius: number;
+ rings: Ring[];
+ quadrants: Quadrant[];
+ entries: Entry[];
+ activeEntry?: Entry;
+ onEntryMouseEnter?: (entry: Entry) => void;
+ onEntryMouseLeave?: (entry: Entry) => void;
+};
+
+// A component that draws the radar circle.
+const RadarPlot: FC = props => {
+ const {
+ width,
+ height,
+ radius,
+ quadrants,
+ rings,
+ entries,
+ activeEntry,
+ onEntryMouseEnter,
+ onEntryMouseLeave,
+ } = props;
+
+ return (
+
+ onEntryMouseEnter(entry))
+ }
+ onEntryMouseLeave={
+ onEntryMouseLeave && (entry => onEntryMouseLeave(entry))
+ }
+ />
+
+
+
+ {entries.map(entry => (
+ onEntryMouseEnter(entry))}
+ onMouseLeave={onEntryMouseLeave && (() => onEntryMouseLeave(entry))}
+ />
+ ))}
+
+
+
+ );
+};
+
+export default RadarPlot;
diff --git a/plugins/tech-radar/src/sampleData.ts b/plugins/tech-radar/src/sampleData.ts
index 8372e8a69f..5976d07559 100644
--- a/plugins/tech-radar/src/sampleData.ts
+++ b/plugins/tech-radar/src/sampleData.ts
@@ -36,48 +36,48 @@ quadrants.push({ id: 'process', name: 'Process' });
const entries = new Array();
entries.push({
moved: 0,
- ring: 'use',
+ ring: { id: 'use', name: 'USE', color: '#93c47d' },
url: '#',
key: 'javascript',
id: 'javascript',
title: 'JavaScript',
- quadrant: 'languages',
+ quadrant: { id: 'languages', name: 'Languages' },
});
entries.push({
moved: 0,
- ring: 'use',
+ ring: { id: 'use', name: 'USE', color: '#93c47d' },
url: '#',
key: 'typescript',
id: 'typescript',
title: 'TypeScript',
- quadrant: 'languages',
+ quadrant: { id: 'languages', name: 'Languages' },
});
entries.push({
moved: 0,
- ring: 'use',
+ ring: { id: 'use', name: 'USE', color: '#93c47d' },
url: '#',
key: 'webpack',
id: 'webpack',
title: 'Webpack',
- quadrant: 'frameworks',
+ quadrant: { id: 'frameworks', name: 'Frameworks' },
});
entries.push({
moved: 0,
- ring: 'use',
+ ring: { id: 'use', name: 'USE', color: '#93c47d' },
url: '#',
key: 'react',
id: 'react',
title: 'React',
- quadrant: 'frameworks',
+ quadrant: { id: 'frameworks', name: 'Frameworks' },
});
entries.push({
moved: 0,
- ring: 'use',
+ ring: { id: 'use', name: 'USE', color: '#93c47d' },
url: '#',
key: 'code-reviews',
id: 'code-reviews',
title: 'Code Reviews',
- quadrant: 'process',
+ quadrant: { id: 'process', name: 'Process' },
});
entries.push({
moved: 0,
@@ -85,17 +85,17 @@ entries.push({
key: 'mob-programming',
id: 'mob-programming',
title: 'Mob Programming',
- quadrant: 'process',
- ring: 'assess',
+ quadrant: { id: 'process', name: 'Process' },
+ ring: { id: 'assess', name: 'ASSESS', color: '#fbdb84' },
});
entries.push({
moved: 0,
- ring: 'use',
+ ring: { id: 'use', name: 'USE', color: '#93c47d' },
url: '#',
key: 'github-actions',
id: 'github-actions',
title: 'GitHub Actions',
- quadrant: 'infrastructure',
+ quadrant: { id: 'infrastructure', name: 'Infrastructure' },
});
export default function getSampleData(): Promise {
diff --git a/plugins/tech-radar/src/utils/prop-types.js b/plugins/tech-radar/src/utils/types.ts
similarity index 57%
rename from plugins/tech-radar/src/utils/prop-types.js
rename to plugins/tech-radar/src/utils/types.ts
index d5491435b6..14a0554a1f 100644
--- a/plugins/tech-radar/src/utils/prop-types.js
+++ b/plugins/tech-radar/src/utils/types.ts
@@ -14,40 +14,59 @@
* limitations under the License.
*/
-import PropTypes from 'prop-types';
-
// Parameters for a ring; its index in an array determines how close to the center this ring is.
-export const RING = {
- id: PropTypes.string.isRequired,
- idx: PropTypes.number,
- name: PropTypes.string.isRequired,
- color: PropTypes.string.isRequired,
+export type Ring = {
+ id: string;
+ idx?: number;
+ name: string;
+ color: string;
+ outerRadius?: number;
+ innerRadius?: number;
};
// Parameters for a quadrant (there should be exactly 4 of course)
-export const QUADRANT = {
- id: PropTypes.string.isRequired,
- idx: PropTypes.number,
- name: PropTypes.string.isRequired,
+export type Quadrant = {
+ id: string;
+ idx?: number;
+ name: string;
+ legendX?: number;
+ legendY?: number;
+ legendWidth?: number;
+ legendHeight?: number;
+ radialMin?: number;
+ radialMax?: number;
+ offsetX?: number;
+ offsetY?: number;
};
-export const ENTRY = {
- id: PropTypes.string.isRequired,
- idx: PropTypes.number,
+export type Segment = {
+ clipx: Function;
+ clipy: Function;
+ random: Function;
+};
+
+export type Entry = {
+ id: string;
+ idx?: number;
+ x?: number;
+ y?: number;
+ color?: string;
+ segment?: Segment;
// The quadrant where this entry belongs
- quadrant: PropTypes.shape(QUADRANT).isRequired,
+ quadrant: Quadrant;
// The ring where this entry belongs
- ring: PropTypes.shape(RING).isRequired,
+ ring: Ring;
// The label that's shown in the legend and on hover
- title: PropTypes.string.isRequired,
+ title: string;
// An URL to a longer description as to why this entry is where it is
- url: PropTypes.string,
+ url?: string;
// How this entry has recently moved; -1 for "down", +1 for "up", 0 for not moved
- moved: PropTypes.number,
+ moved?: number;
+ active?: boolean;
};
-// The same as ENTRY except quadrant/ring are declared by their string ID instead of being the actual objects
-export const DECLARED_ENTRY = Object.assign({}, ENTRY, {
- quadrant: PropTypes.string.isRequired,
- ring: PropTypes.string.isRequired,
-});
+// The same as Entry except quadrant/ring are declared by their string ID instead of being the actual objects
+export type DeclaredEntry = Entry & {
+ quadrant: string;
+ ring: string;
+};
diff --git a/yarn.lock b/yarn.lock
index b3f5b701c0..ee44aa0678 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2665,17 +2665,6 @@
telejson "^3.2.0"
util-deprecate "^1.0.2"
-"@storybook/channel-postmessage@5.3.18":
- version "5.3.18"
- resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-5.3.18.tgz#93d46740b5cc9b36ddd073f0715b54c4959953bf"
- integrity sha512-awxBW/aVfNtY9QvYZgsPaMXgUpC2+W3vEyQcl/w4ce0YVH+7yWx3wt3Ku49lQwxZwDrxP3QoC0U+mkPc9hBJwA==
- dependencies:
- "@storybook/channels" "5.3.18"
- "@storybook/client-logger" "5.3.18"
- core-js "^3.0.1"
- global "^4.3.2"
- telejson "^3.2.0"
-
"@storybook/channel-postmessage@5.3.19":
version "5.3.19"
resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-5.3.19.tgz#ef9fe974c2a529d89ce342ff7acf5cc22805bae9"
@@ -2701,29 +2690,6 @@
dependencies:
core-js "^3.0.1"
-"@storybook/client-api@5.3.18":
- version "5.3.18"
- resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-5.3.18.tgz#e71041796f95888de0e4524734418e6b120b060a"
- integrity sha512-QiXTDUpjdyW19BlocLw07DrkOnEzVaWGJcRze2nSs29IKKuq1Ncv2LOAZt6ySSq0PmIKsjBou3bmS1/aXmDMdw==
- dependencies:
- "@storybook/addons" "5.3.18"
- "@storybook/channel-postmessage" "5.3.18"
- "@storybook/channels" "5.3.18"
- "@storybook/client-logger" "5.3.18"
- "@storybook/core-events" "5.3.18"
- "@storybook/csf" "0.0.1"
- "@types/webpack-env" "^1.15.0"
- core-js "^3.0.1"
- eventemitter3 "^4.0.0"
- global "^4.3.2"
- is-plain-object "^3.0.0"
- lodash "^4.17.15"
- memoizerific "^1.11.3"
- qs "^6.6.0"
- stable "^0.1.8"
- ts-dedent "^1.1.0"
- util-deprecate "^1.0.2"
-
"@storybook/client-api@5.3.19":
version "5.3.19"
resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-5.3.19.tgz#7a5630bb8fffb92742b1773881e9004ee7fdf8e0"
@@ -2761,33 +2727,6 @@
dependencies:
core-js "^3.0.1"
-"@storybook/components@5.3.18":
- version "5.3.18"
- resolved "https://registry.npmjs.org/@storybook/components/-/components-5.3.18.tgz#528f6ab1660981e948993a04b407a6fad7751589"
- integrity sha512-LIN4aVCCDY7klOwtuqQhfYz4tHaMADhXEzZpij+3r8N68Inck6IJ1oo9A9umXQPsTioQi8e6FLobH1im90j/2A==
- dependencies:
- "@storybook/client-logger" "5.3.18"
- "@storybook/theming" "5.3.18"
- "@types/react-syntax-highlighter" "11.0.4"
- "@types/react-textarea-autosize" "^4.3.3"
- core-js "^3.0.1"
- global "^4.3.2"
- lodash "^4.17.15"
- markdown-to-jsx "^6.9.1"
- memoizerific "^1.11.3"
- polished "^3.3.1"
- popper.js "^1.14.7"
- prop-types "^15.7.2"
- react "^16.8.3"
- react-dom "^16.8.3"
- react-focus-lock "^2.1.0"
- react-helmet-async "^1.0.2"
- react-popper-tooltip "^2.8.3"
- react-syntax-highlighter "^11.0.2"
- react-textarea-autosize "^7.1.0"
- simplebar-react "^1.0.0-alpha.6"
- ts-dedent "^1.1.0"
-
"@storybook/components@5.3.19":
version "5.3.19"
resolved "https://registry.npmjs.org/@storybook/components/-/components-5.3.19.tgz#aac1f9eea1247cc85bd93b10fca803876fb84a6b"
@@ -3739,6 +3678,16 @@
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
+"@types/json5@^0.0.29":
+ version "0.0.29"
+ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
+ integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
+
+"@types/jwt-decode@2.2.1":
+ version "2.2.1"
+ resolved "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz#afdf5c527fcfccbd4009b5fd02d1e18241f2d2f2"
+ integrity sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A==
+
"@types/lodash@^4.14.151":
version "4.14.155"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a"
@@ -4880,7 +4829,7 @@ array-unique@^0.3.2:
resolved "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
-array.prototype.flat@^1.2.1:
+array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.3:
version "1.2.3"
resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==
@@ -8157,7 +8106,7 @@ eslint-config-prettier@^6.0.0:
dependencies:
get-stdin "^6.0.0"
-eslint-import-resolver-node@^0.3.2:
+eslint-import-resolver-node@^0.3.3:
version "0.3.3"
resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404"
integrity sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==
@@ -8165,7 +8114,7 @@ eslint-import-resolver-node@^0.3.2:
debug "^2.6.9"
resolve "^1.13.1"
-eslint-module-utils@^2.1.1:
+eslint-module-utils@^2.1.1, eslint-module-utils@^2.6.0:
version "2.6.0"
resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6"
integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==
@@ -8173,38 +8122,31 @@ eslint-module-utils@^2.1.1:
debug "^2.6.9"
pkg-dir "^2.0.0"
-eslint-module-utils@^2.4.1:
- version "2.5.2"
- resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz#7878f7504824e1b857dd2505b59a8e5eda26a708"
- integrity sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==
- dependencies:
- debug "^2.6.9"
- pkg-dir "^2.0.0"
-
eslint-plugin-cypress@^2.10.3:
- version "2.10.3"
- resolved "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.10.3.tgz#82eba7e014954149d590402eecd0d4e147cc7f14"
- integrity sha512-CvFeoCquShfO8gHNIKA1VpUTz78WtknMebLemBd1lRbcmJNjwpqCqpQYUG/XVja8GjdX/e2TJXYa+EUBxehtUg==
+ version "2.11.1"
+ resolved "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.1.tgz#a945e2774b88211e2c706a059d431e262b5c2862"
+ integrity sha512-MxMYoReSO5+IZMGgpBZHHSx64zYPSPTpXDwsgW7ChlJTF/sA+obqRbHplxD6sBStE+g4Mi0LCLkG4t9liu//mQ==
dependencies:
globals "^11.12.0"
eslint-plugin-import@^2.20.2:
- version "2.20.2"
- resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d"
- integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg==
+ version "2.21.1"
+ resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.21.1.tgz#3398318e5e4abbd23395c4964ce61538705154c8"
+ integrity sha512-qYOOsgUv63vHof7BqbzuD+Ud34bXHxFJxntuAC1ZappFZXYbRIek3aJ7jc9i2dHDGDyZ/0zlO0cpioES265Lsw==
dependencies:
- array-includes "^3.0.3"
- array.prototype.flat "^1.2.1"
+ array-includes "^3.1.1"
+ array.prototype.flat "^1.2.3"
contains-path "^0.1.0"
debug "^2.6.9"
doctrine "1.5.0"
- eslint-import-resolver-node "^0.3.2"
- eslint-module-utils "^2.4.1"
+ eslint-import-resolver-node "^0.3.3"
+ eslint-module-utils "^2.6.0"
has "^1.0.3"
minimatch "^3.0.4"
- object.values "^1.1.0"
+ object.values "^1.1.1"
read-pkg-up "^2.0.0"
- resolve "^1.12.0"
+ resolve "^1.17.0"
+ tsconfig-paths "^3.9.0"
eslint-plugin-jest@^23.6.0:
version "23.8.2"
@@ -11954,6 +11896,11 @@ jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3:
array-includes "^3.0.3"
object.assign "^4.1.0"
+jwt-decode@2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
+ integrity sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=
+
keyv@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
@@ -12664,14 +12611,6 @@ markdown-to-jsx@^6.11.4:
prop-types "^15.6.2"
unquote "^1.1.0"
-markdown-to-jsx@^6.9.1:
- version "6.11.0"
- resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.0.tgz#a2e3f2bc781c3402d8bb0f8e0a12a186474623b0"
- integrity sha512-RH7LCJQ4RFmPqVeZEesKaO1biRzB/k4utoofmTCp3Eiw6D7qfvK8fzZq/2bjEJAtVkfPrM5SMt5APGf2rnaKMg==
- dependencies:
- prop-types "^15.6.2"
- unquote "^1.1.0"
-
material-table@^1.58.0:
version "1.58.2"
resolved "https://registry.npmjs.org/material-table/-/material-table-1.58.2.tgz#dc0d19652848e6bb92f747d122bd7d4681cca6dc"
@@ -18248,6 +18187,16 @@ tsc-watch@^4.2.3:
string-argv "^0.1.1"
strip-ansi "^4.0.0"
+tsconfig-paths@^3.9.0:
+ version "3.9.0"
+ resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
+ integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==
+ dependencies:
+ "@types/json5" "^0.0.29"
+ json5 "^1.0.1"
+ minimist "^1.2.0"
+ strip-bom "^3.0.0"
+
tslib@1.10.0:
version "1.10.0"
resolved "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
@@ -19058,9 +19007,9 @@ websocket-driver@>=0.5.1:
websocket-extensions ">=0.1.1"
websocket-extensions@>=0.1.1:
- version "0.1.3"
- resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
- integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
+ integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
version "1.0.5"