diff --git a/.changeset/tough-moles-whisper.md b/.changeset/tough-moles-whisper.md new file mode 100644 index 0000000000..14dd76c116 --- /dev/null +++ b/.changeset/tough-moles-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Added the ability to display a timeline to each entry in the details box diff --git a/plugins/tech-radar/README.md b/plugins/tech-radar/README.md index 5c99541c2a..1c64e4fc9e 100644 --- a/plugins/tech-radar/README.md +++ b/plugins/tech-radar/README.md @@ -67,6 +67,127 @@ export interface TechRadarPageProps { When defining the radar entries you can see the available properties on the file [api](./src/api.ts) +## Tech radar data model + +| Name | Type | Description | Required? | +| ----------- | ----------------------- | -------------------------------------------------------------------- | --------- | +| `title` | string | The title of the radar | Yes | +| `quadrants` | [quadrant[]](#quadrant) | The 4 quadrants of the radar, clockwise starting at the bottom right | Yes | +| `rings` | [ring[]](#ring) | The radar rings, starting from the inside | Yes | +| `entries` | [entry[]](#entry) | The radar entries | Yes | + +### quadrant + +| Name | Type | Description | Required? | +| ------ | ------ | ------------------------ | --------- | +| `id` | string | The id of the quadrant | Yes | +| `name` | string | The name of the quadrant | Yes | + +### ring + +| Name | Type | Description | Required? | +| ------- | ------ | ------------------------------------------------- | --------- | +| `id` | string | The id of the ring | Yes | +| `name` | string | The name of the ring | Yes | +| `color` | string | The color of the ring and entries inside the ring | Yes | + +### entry + +| Name | Type | Description | Required? | +| ------------- | ----------------------- | ----------------------------------------------- | --------- | +| `id` | string | The unique id from the entry | Yes | +| `title` | string | The title that is shown in the radar | Yes | +| `description` | string | The full description of the entry | No | +| key | string | The entry key | Yes | +| `url` | string | The URL to the entry internal or external page | No | +| `quadrant` | string | The name of the quadrant connected to the entry | Yes | +| `timeline` | [timeline[]](#timeline) | Requires minimal one timeline entry | Yes | + +### timeline + +| Name | Type | Description | Required? | +| ------------- | ------ | ------------------------------------------------------------- | --------- | +| `moved` | number | Possible values are: -1 (moved out), 0 (stayed), 1 (moved in) | Yes | +| `ringId` | string | The ring id | Yes | +| `date` | string | Date in format (YYYY-MM-dd) | Yes | +| `description` | string | A long description | Yes | + +### Sample + +This is a sample on how the JSON file could look like. +The TS example can be found [here](src/sample.ts). + +```JSON +{ + "title": "Title of your Tech radar", + "quadrants": [ + { + "id": "1", + "name": "Bottom right" + }, + { + "id": "2", + "name": "Bottom left" + }, + { + "id": "3", + "name": "Top left" + }, + { + "id": "4", + "name": "Top right" + } + ], + "rings": [ + { + "id": "adopt", + "name": "ADOPT", + "color": "#93c47d" + }, + { + "id": "trial", + "name": "TRIAL", + "color": "#93d2c2" + }, + { + "id": "assess", + "name": "ASSESS", + "color": "#fbdb84" + }, + { + "id": "hold", + "name": "HOLD", + "color": "#efafa9" + } + ], + "entries": [ + { + "id": "typescript", + "title": "Typescript", + "description": "Long description for Typescript", + "key": "typescript", + "url": "#", + "quadrant": "1", + "timeline": [ + { + "moved": 0, + "ringId": "trial", + "date": "2022-02-06", + "description": "Long description for trial" + }, + { + "moved": 1, + "ringId": "adopt", + "date": "2022-02-08", + "description": "Long description for adopt" + } + ] + }, + ... + ] +} +``` + ## Frequently Asked Questions ### Who created the Tech Radar? diff --git a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx index 51fed918ec..dd6cd66f87 100644 --- a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx +++ b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx @@ -19,11 +19,22 @@ import { screen } from '@testing-library/react'; import { Props, RadarDescription } from './RadarDescription'; import { renderInTestApp } from '@backstage/test-utils'; +import { Ring } from '../../utils/types'; + +const ring: Ring = { + id: 'example-ring', + name: 'example-ring', + color: 'red', +}; const minProps: Props = { open: true, title: 'example-title', description: 'example-description', + timeline: [ + { date: new Date(), ring: ring, description: 'test timeline 1' }, + { date: new Date(), ring: ring, description: 'test timeline 2' }, + ], links: [{ url: 'https://example.com/docs', title: 'example-link' }], onClose: () => {}, }; diff --git a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx index 92b15d985a..df39c2ce55 100644 --- a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx +++ b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx @@ -21,12 +21,15 @@ import { Button, DialogActions, DialogContent } from '@material-ui/core'; import LinkIcon from '@material-ui/icons/Link'; import { Link, MarkdownContent } from '@backstage/core-components'; import { isValidUrl } from '../../utils/components'; +import type { EntrySnapshot } from '../../utils/types'; +import { RadarTimeline } from '../RadarTimeline'; export type Props = { open: boolean; onClose: () => void; title: string; description: string; + timeline?: EntrySnapshot[]; url?: string; links?: Array<{ url: string; title: string }>; }; @@ -39,7 +42,7 @@ const RadarDescription = (props: Props): JSX.Element => { return isValidUrl(url) || Boolean(links && links.length > 0); } - const { open, onClose, title, description, url, links } = props; + const { open, onClose, title, description, timeline, url, links } = props; return ( @@ -48,6 +51,7 @@ const RadarDescription = (props: Props): JSX.Element => { + {showDialogActions(url, links) && ( diff --git a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx index 829b9c7708..af9e840ead 100644 --- a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx +++ b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { makeStyles, Theme } from '@material-ui/core'; import { WithLink } from '../../utils/components'; import { RadarDescription } from '../RadarDescription'; +import type { EntrySnapshot } from '../../utils/types'; export type Props = { x: number; @@ -27,6 +28,7 @@ export type Props = { url?: string; moved?: number; description?: string; + timeline?: EntrySnapshot[]; title?: string; onMouseEnter?: (event: React.MouseEvent) => void; onMouseLeave?: (event: React.MouseEvent) => void; @@ -67,6 +69,7 @@ const RadarEntry = (props: Props): JSX.Element => { const { moved, description, + timeline, title, color, url, @@ -107,6 +110,7 @@ const RadarEntry = (props: Props): JSX.Element => { onClose={handleClose} title={title ? title : 'no title'} description={description ? description : 'no description'} + timeline={timeline ? timeline : []} url={url} /> )} diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx index 7e31ddddd5..4273963690 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx @@ -18,6 +18,7 @@ import Typography from '@material-ui/core/Typography'; import React from 'react'; import { WithLink } from '../../utils/components'; import { RadarDescription } from '../RadarDescription'; +import type { EntrySnapshot } from '../../utils/types'; type RadarLegendLinkProps = { url?: string; @@ -26,6 +27,7 @@ type RadarLegendLinkProps = { classes: ClassNameMap; active?: boolean; links: Array<{ url: string; title: string }>; + timeline: EntrySnapshot[]; }; export const RadarLegendLink = ({ @@ -35,6 +37,7 @@ export const RadarLegendLink = ({ classes, active, links, + timeline, }: RadarLegendLinkProps) => { const [open, setOpen] = React.useState(false); @@ -75,6 +78,7 @@ export const RadarLegendLink = ({ title={title ? title : 'no title'} url={url} description={description} + timeline={timeline ? timeline : []} links={links} /> )} diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx index da5c10831e..2005d21915 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx @@ -68,6 +68,7 @@ export const RadarLegendRing = ({ description={entry.description} active={entry.active} links={entry.links ?? []} + timeline={entry.timeline ?? []} /> ))} diff --git a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx index d9788be887..957017692b 100644 --- a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx +++ b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx @@ -76,6 +76,7 @@ const RadarPlot = (props: Props): JSX.Element => { description={entry.description} moved={entry.moved} title={entry.title} + timeline={entry.timeline} onMouseEnter={onEntryMouseEnter && (() => onEntryMouseEnter(entry))} onMouseLeave={onEntryMouseLeave && (() => onEntryMouseLeave(entry))} /> diff --git a/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.test.tsx b/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.test.tsx new file mode 100644 index 0000000000..e5142a7af2 --- /dev/null +++ b/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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 { screen } from '@testing-library/react'; + +import { Props, RadarTimeline } from './RadarTimeline'; +import { renderInTestApp } from '@backstage/test-utils'; +import { Ring } from '../../utils/types'; + +const ring: Ring = { + id: 'example-ring', + name: 'example-ring', + color: 'red', +}; + +const minProps: Props = { + timeline: [ + { date: new Date(), ring: ring, description: 'test timeline 1' }, + { date: new Date(), ring: ring, description: 'test timeline 2' }, + ], +}; + +describe('RadarDescription', () => { + it('should render', async () => { + await renderInTestApp(); + expect(screen.getByText(String('test timeline 1'))).toBeInTheDocument(); + expect(screen.getByText(String('test timeline 2'))).toBeInTheDocument(); + }); +}); diff --git a/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx b/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx new file mode 100644 index 0000000000..555873cf94 --- /dev/null +++ b/plugins/tech-radar/src/components/RadarTimeline/RadarTimeline.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2020 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 type { EntrySnapshot } from '../../utils/types'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; + +import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; +import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward'; +import AdjustIcon from '@material-ui/icons/Adjust'; + +import { MovedState } from '../../api'; + +export type Props = { + timeline?: EntrySnapshot[]; +}; + +const RadarTimeline = (props: Props): JSX.Element => { + const { timeline } = props; + + return ( + <> + + History + + + + + + Moved in direction + Moved to ring + Moved on date + Description + + + + {timeline?.length === 0 && ( + + + No Timeline + + + )} + {timeline?.map(timeEntry => ( + + + {timeEntry.moved === MovedState.Up ? : ''} + {timeEntry.moved === MovedState.Down ? ( + + ) : ( + '' + )} + {timeEntry.moved === MovedState.NoChange ? ( + + ) : ( + '' + )} + + + {timeEntry.ring.name ? timeEntry.ring.name : ''} + + + {timeEntry.date.toLocaleDateString() + ? timeEntry.date.toLocaleDateString() + : ''} + + + {timeEntry.description ? timeEntry.description : ''} + + + ))} + +
+
+ + ); +}; + +export { RadarTimeline }; diff --git a/plugins/tech-radar/src/components/RadarTimeline/index.ts b/plugins/tech-radar/src/components/RadarTimeline/index.ts new file mode 100644 index 0000000000..cdf156ce24 --- /dev/null +++ b/plugins/tech-radar/src/components/RadarTimeline/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 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 { RadarTimeline } from './RadarTimeline'; +export type { Props } from './RadarTimeline'; diff --git a/plugins/tech-radar/src/sample.ts b/plugins/tech-radar/src/sample.ts index a967d2cbe7..9f6d65a683 100644 --- a/plugins/tech-radar/src/sample.ts +++ b/plugins/tech-radar/src/sample.ts @@ -177,6 +177,11 @@ entries.push({ date: new Date('2020-08-06'), description: 'long description', }, + { + ringId: 'trial', + date: new Date('2020-07-05'), + description: 'long description', + }, ], links: [ {