Using EmptyState in Github Actions Plugin (#2680)
* display EmptyState when no CI/CD is avialable * change padding and title size * add EmptyState to gitHubActionPlugin * fix test failing * add MissingAnnotationEmptyState * add MissingAnnotationEmptyState to storybook * add highlightedNumbers feature to Codesnippet * use the new component in github actions and update README * remove entity from MissingAnnotationEmptyState * remove proptypes from Codesnippet Co-authored-by: Samira Mokaram <samiram@spotify.com>
This commit is contained in:
@@ -47,8 +47,8 @@ import {
|
||||
useEntity,
|
||||
} from '@backstage/plugin-catalog';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { WarningPanel } from '@backstage/core';
|
||||
import { Button, Grid } from '@material-ui/core';
|
||||
import { EmptyState } from '@backstage/core';
|
||||
|
||||
const CICDSwitcher = ({ entity }: { entity: Entity }) => {
|
||||
// This component is just an example of how you can implement your company's logic in entity page.
|
||||
@@ -66,10 +66,20 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => {
|
||||
return <TravisCIRouter entity={entity} />;
|
||||
default:
|
||||
return (
|
||||
<WarningPanel title="CI/CD switcher:">
|
||||
No CI/CD is available for this entity. Check corresponding
|
||||
annotations!
|
||||
</WarningPanel>
|
||||
<EmptyState
|
||||
title="No CI/CD available for this entity"
|
||||
missing="info"
|
||||
description="You need to add an annotation to your component if you want to enable CI/CD for it. You can read more about annotations in Backstage by clicking the button below."
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href="https://backstage.io/docs/features/software-catalog/well-known-annotations"
|
||||
>
|
||||
Read more
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { docco, dark } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
|
||||
import { useTheme } from '@material-ui/core';
|
||||
@@ -27,19 +26,16 @@ type Props = {
|
||||
language: string;
|
||||
showLineNumbers?: boolean;
|
||||
showCopyCodeButton?: boolean;
|
||||
highlightedNumbers?: number[];
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
showLineNumbers: false,
|
||||
showCopyCodeButton: false,
|
||||
};
|
||||
|
||||
export const CodeSnippet: FC<Props> = props => {
|
||||
const { text, language, showLineNumbers, showCopyCodeButton } = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
|
||||
export const CodeSnippet = ({
|
||||
text,
|
||||
language,
|
||||
showLineNumbers = false,
|
||||
showCopyCodeButton = false,
|
||||
highlightedNumbers,
|
||||
}: Props) => {
|
||||
const theme = useTheme<BackstageTheme>();
|
||||
const mode = theme.palette.type === 'dark' ? dark : docco;
|
||||
|
||||
@@ -49,6 +45,12 @@ export const CodeSnippet: FC<Props> = props => {
|
||||
language={language}
|
||||
style={mode}
|
||||
showLineNumbers={showLineNumbers}
|
||||
wrapLines
|
||||
lineProps={(lineNumber: number) =>
|
||||
highlightedNumbers?.includes(lineNumber)
|
||||
? { style: { backgroundColor: '#e6ffed' } }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</SyntaxHighlighter>
|
||||
@@ -60,11 +62,3 @@ export const CodeSnippet: FC<Props> = props => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Type check for the JS files using this core component
|
||||
CodeSnippet.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
showLineNumbers: PropTypes.bool,
|
||||
showCopyCodeButton: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import React from 'react';
|
||||
import { EmptyState } from './EmptyState';
|
||||
import { Button } from '@material-ui/core';
|
||||
import { MissingAnnotationEmptyState } from './MissingAnnotationEmptyState';
|
||||
|
||||
export default {
|
||||
title: 'EmptyState',
|
||||
@@ -25,7 +26,7 @@ export default {
|
||||
|
||||
const containerStyle = { width: '100%', height: '100vh' };
|
||||
|
||||
export const MissingAnnotation = () => (
|
||||
export const Field = () => (
|
||||
<div style={containerStyle}>
|
||||
<EmptyState
|
||||
missing="field"
|
||||
@@ -35,7 +36,7 @@ export const MissingAnnotation = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
export const NoInformation = () => (
|
||||
export const Info = () => (
|
||||
<div style={containerStyle}>
|
||||
<EmptyState
|
||||
missing="info"
|
||||
@@ -45,7 +46,7 @@ export const NoInformation = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
export const CreateComponent = () => (
|
||||
export const Content = () => (
|
||||
<div style={containerStyle}>
|
||||
<EmptyState
|
||||
missing="content"
|
||||
@@ -55,7 +56,7 @@ export const CreateComponent = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
export const NoBuild = () => (
|
||||
export const Data = () => (
|
||||
<div style={containerStyle}>
|
||||
<EmptyState
|
||||
missing="data"
|
||||
@@ -79,3 +80,9 @@ export const WithAction = () => (
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const MissingAnnotation = () => (
|
||||
<div style={containerStyle}>
|
||||
<MissingAnnotationEmptyState annotation="backstage.io/example" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import Background from './assets/Background.svg';
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
padding: theme.spacing(10, 0, 0, 0),
|
||||
padding: theme.spacing(2, 0, 0, 0),
|
||||
},
|
||||
action: {
|
||||
marginTop: theme.spacing(2),
|
||||
@@ -56,7 +56,7 @@ export const EmptyState = ({ title, description, missing, action }: Props) => {
|
||||
>
|
||||
<Grid item container direction="column" xs={12} md={6}>
|
||||
<Grid item>
|
||||
<Typography variant="h3">{title}</Typography>
|
||||
<Typography variant="h5">{title}</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="body1">{description}</Typography>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Typography } from '@material-ui/core';
|
||||
import { EmptyState } from './EmptyState';
|
||||
import { CodeSnippet } from '../CodeSnippet';
|
||||
|
||||
const COMPONENT_YAML = `# Example
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage
|
||||
description: backstage.io
|
||||
annotations:
|
||||
ANNOTATION: value
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: production
|
||||
owner: guest
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
annotation: string;
|
||||
};
|
||||
|
||||
export const MissingAnnotationEmptyState = ({ annotation }: Props) => {
|
||||
return (
|
||||
<EmptyState
|
||||
missing="field"
|
||||
title="Missing Annotation"
|
||||
description={`The "${annotation}" annotation is missing. You need to add the annotation to your component if you want to enable this tool for it.`}
|
||||
action={
|
||||
<>
|
||||
<Typography variant="body1">
|
||||
Add the annotation to your component YAML as per the highlighted
|
||||
example below:
|
||||
</Typography>
|
||||
<CodeSnippet
|
||||
text={COMPONENT_YAML.replace('ANNOTATION', annotation)}
|
||||
language="yaml"
|
||||
showLineNumbers
|
||||
highlightedNumbers={[7, 8]}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href="https://backstage.io/docs/features/software-catalog/well-known-annotations"
|
||||
>
|
||||
Read more
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { EmptyState } from './EmptyState';
|
||||
export { MissingAnnotationEmptyState } from './MissingAnnotationEmptyState';
|
||||
|
||||
@@ -11,7 +11,7 @@ TBD
|
||||
### Generic Requirements
|
||||
|
||||
1. Provide OAuth credentials:
|
||||
1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) with callback URL set to `https://localhost:3000/auth/github`.
|
||||
1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) with callback URL set to `http://localhost:7000/api/auth/github`.
|
||||
2. Take Client ID and Client Secret from the newly created app's settings page and put them into `AUTH_GITHUB_CLIENT_ID` and `AUTH_GITHUB_CLIENT_SECRET` env variables.
|
||||
2. Annotate your component with a correct GitHub Actions repository and owner:
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ import { errorApiRef, useApi } from '@backstage/core-api';
|
||||
import { GITHUB_ACTIONS_ANNOTATION } from '../useProjectName';
|
||||
import { useWorkflowRuns } from '../useWorkflowRuns';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Table } from '@backstage/core';
|
||||
import { EmptyState, Table } from '@backstage/core';
|
||||
import { WorkflowRunStatus } from '../WorkflowRunStatus';
|
||||
import { Card, Link, TableContainer } from '@material-ui/core';
|
||||
import { Button, Card, Link, TableContainer } from '@material-ui/core';
|
||||
import { generatePath, Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
const firstLine = (message: string): string => message.split('\n')[0];
|
||||
@@ -54,7 +54,22 @@ export const RecentWorkflowRunsCard = ({
|
||||
}
|
||||
}, [error, errorApi]);
|
||||
|
||||
return (
|
||||
return !runs.length ? (
|
||||
<EmptyState
|
||||
missing="data"
|
||||
title="No Workflow Data"
|
||||
description="This component has Github Actions enabled, but no data was found. Have you created any Workflows? Click the button below to create a new Workflow."
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href={`https://github.com/${owner}/${repo}/actions/new`}
|
||||
>
|
||||
Create new Workflow
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<TableContainer component={Card}>
|
||||
<Table
|
||||
title="Recent Workflow Runs"
|
||||
|
||||
@@ -20,18 +20,14 @@ import { rootRouteRef, buildRouteRef } from '../plugin';
|
||||
import { WorkflowRunDetails } from './WorkflowRunDetails';
|
||||
import { WorkflowRunsTable } from './WorkflowRunsTable';
|
||||
import { GITHUB_ACTIONS_ANNOTATION } from './useProjectName';
|
||||
import { WarningPanel } from '@backstage/core';
|
||||
import { MissingAnnotationEmptyState } from '@backstage/core';
|
||||
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]);
|
||||
|
||||
export const Router = ({ entity }: { entity: Entity }) =>
|
||||
// TODO(shmidt-i): move warning to a separate standardized component
|
||||
!isPluginApplicableToEntity(entity) ? (
|
||||
<WarningPanel title="GitHubActions plugin:">
|
||||
<pre>{GITHUB_ACTIONS_ANNOTATION}</pre> annotation is missing on the
|
||||
entity.
|
||||
</WarningPanel>
|
||||
<MissingAnnotationEmptyState annotation={GITHUB_ACTIONS_ANNOTATION} />
|
||||
) : (
|
||||
<Routes>
|
||||
<Route
|
||||
|
||||
@@ -14,11 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC } from 'react';
|
||||
import { Link, Typography, Box, IconButton, Tooltip } from '@material-ui/core';
|
||||
import {
|
||||
Link,
|
||||
Typography,
|
||||
Box,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Button,
|
||||
} from '@material-ui/core';
|
||||
import RetryIcon from '@material-ui/icons/Replay';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import { Link as RouterLink, generatePath } from 'react-router-dom';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { EmptyState, Table, TableColumn } from '@backstage/core';
|
||||
import { useWorkflowRuns } from '../useWorkflowRuns';
|
||||
import { WorkflowRunStatus } from '../WorkflowRunStatus';
|
||||
import SyncIcon from '@material-ui/icons/Sync';
|
||||
@@ -156,15 +163,34 @@ export const WorkflowRunsTable = ({
|
||||
}) => {
|
||||
const { value: projectName, loading } = useProjectName(entity);
|
||||
const [owner, repo] = (projectName ?? '/').split('/');
|
||||
const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns({
|
||||
const [
|
||||
{ runs, ...tableProps },
|
||||
{ retry, setPage, setPageSize },
|
||||
] = useWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
});
|
||||
|
||||
return (
|
||||
return !runs ? (
|
||||
<EmptyState
|
||||
missing="data"
|
||||
title="No Workflow Data"
|
||||
description="This component has Github Actions enabled, but no data was found. Have you created any Workflows? Click the button below to create a new Workflow."
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href={`https://github.com/${projectName}/actions/new`}
|
||||
>
|
||||
Create new Workflow
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<WorkflowRunsTableView
|
||||
{...tableProps}
|
||||
runs={runs}
|
||||
loading={loading || tableProps.loading}
|
||||
retry={retry}
|
||||
onChangePageSize={setPageSize}
|
||||
|
||||
Reference in New Issue
Block a user