Merge pull request #17295 from joosthofman/feature/tech-radar-timelines
Radar entry timeline added to the entry detail page
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-tech-radar': patch
|
||||
---
|
||||
|
||||
Added the ability to display a timeline to each entry in the details box
|
||||
@@ -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';
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user