Merge pull request #17295 from joosthofman/feature/tech-radar-timelines

Radar entry timeline added to the entry detail page
This commit is contained in:
Ben Lambert
2023-04-25 11:52:38 +02:00
committed by GitHub
12 changed files with 317 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-tech-radar': patch
---
Added the ability to display a timeline to each entry in the details box
+121
View File
@@ -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?
@@ -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: () => {},
};
@@ -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 (
<Dialog data-testid="radar-description" open={open} onClose={onClose}>
@@ -48,6 +51,7 @@ const RadarDescription = (props: Props): JSX.Element => {
</DialogTitle>
<DialogContent dividers>
<MarkdownContent content={description} />
<RadarTimeline timeline={timeline} />
</DialogContent>
{showDialogActions(url, links) && (
<DialogActions>
@@ -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<SVGGElement, MouseEvent>) => void;
onMouseLeave?: (event: React.MouseEvent<SVGGElement, 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}
/>
)}
@@ -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<string>;
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}
/>
)}
@@ -68,6 +68,7 @@ export const RadarLegendRing = ({
description={entry.description}
active={entry.active}
links={entry.links ?? []}
timeline={entry.timeline ?? []}
/>
</li>
))}
@@ -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))}
/>
@@ -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(<RadarTimeline {...minProps} />);
expect(screen.getByText(String('test timeline 1'))).toBeInTheDocument();
expect(screen.getByText(String('test timeline 2'))).toBeInTheDocument();
});
});
@@ -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 (
<>
<Typography variant="h6" gutterBottom>
History
</Typography>
<TableContainer component={Paper}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="left">Moved in direction</TableCell>
<TableCell align="left">Moved to ring</TableCell>
<TableCell align="left">Moved on date</TableCell>
<TableCell align="left">Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{timeline?.length === 0 && (
<TableRow key="no-timeline">
<TableCell component="th" scope="row">
No Timeline
</TableCell>
</TableRow>
)}
{timeline?.map(timeEntry => (
<TableRow key={timeEntry.description}>
<TableCell component="th" scope="row">
{timeEntry.moved === MovedState.Up ? <ArrowUpwardIcon /> : ''}
{timeEntry.moved === MovedState.Down ? (
<ArrowDownwardIcon />
) : (
''
)}
{timeEntry.moved === MovedState.NoChange ? (
<AdjustIcon />
) : (
''
)}
</TableCell>
<TableCell align="left">
{timeEntry.ring.name ? timeEntry.ring.name : ''}
</TableCell>
<TableCell align="left">
{timeEntry.date.toLocaleDateString()
? timeEntry.date.toLocaleDateString()
: ''}
</TableCell>
<TableCell align="left">
{timeEntry.description ? timeEntry.description : ''}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
);
};
export { RadarTimeline };
@@ -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';
+5
View File
@@ -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: [
{