feat: Add StackStorm plugin

Signed-off-by: pamelin <pamelin@expediagroup.com>
This commit is contained in:
pamelin
2023-02-02 16:38:01 +00:00
parent 422e6b050c
commit 67e44a0023
29 changed files with 1275 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+60
View File
@@ -0,0 +1,60 @@
# StackStorm Plugin
Welcome to the StackStorm plugin!
A Backstage integration for the [StackStorm](https://docs.stackstorm.com/overview.html).
This plugin allows you to display a list of executions, view execution details,
browse installed packs, actions and more.
## Getting started
To get started, first you need a running instance of StackStorm.
One of the quickest ways is [running StackStorm with Docker](https://docs.stackstorm.com/install/docker.html).
### Installation
1. Install the plugin with `yarn` in the root of your Backstage directory
```bash
yarn --cwd packages/app add @backstage/plugin-stackstorm
```
2. Import and use the plugin in `packages/app/src/App.tsx`
```tsx
import { StackstormPage } from '@backstage/plugin-stackstorm';
const routes = (
<FlatRoutes>
{/* ...other routes */}
<Route path="/stackstorm" element={<StackstormPage />} />
</FlatRoutes>
```
### Configuration
1. Configure `webUrl` for links to the StackStorm Web UI in `app-config.yaml`.
```yaml
stackstorm:
webUrl: 'https://your.stackstorm.webui.com'
```
2. Configure `stackstorm` proxy
This plugin uses the Backstage proxy to securely communicate with StackStorm API.
Add the following to your `app-config.yaml` to enable this configuration:
```yaml
proxy:
'/stackstorm':
target: https://your.stackstorm.instance.com/api
headers:
St2-Api-Key: ${ST2_API_KEY}
```
In your production deployment of Backstage, you would also need to ensure that
you've set the `ST2_API_KEY` environment variable before starting
the backend.
Read more about how to find or generate this key in the
[StackStorm Authentication Documentation](https://docs.stackstorm.com/authentication.html#api-keys).
+38
View File
@@ -0,0 +1,38 @@
/*
* Copyright 2023 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 interface Config {
stackstorm?: {
/**
* StackStorm API base url
* Used for local testing to bypass CORS
* @visibility frontend
*/
baseUrl?: string;
/**
* StackStorm API key
* Used for local testing to bypass CORS
* Do not set this in production deployment
* @visibility frontend
*/
key?: string;
/**
* StackStorm Web UI url
* Used in links to StackStorm web UI
* @visibility frontend
*/
webUrl?: string;
};
}
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2023 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 { createDevApp } from '@backstage/dev-utils';
import { stackstormPlugin, StackstormPage } from '../src/plugin';
createDevApp()
.registerPlugin(stackstormPlugin)
.addPage({
element: <StackstormPage />,
title: 'Root Page',
path: '/stackstorm',
})
.render();
+65
View File
@@ -0,0 +1,65 @@
{
"name": "@backstage/plugin-stackstorm",
"description": "A Backstage plugin that integrates towards StackStorm",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/stackstorm"
},
"keywords": [
"backstage",
"stackstorm",
"st2"
],
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^0.49.0"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
@@ -0,0 +1,78 @@
/*
* Copyright 2023 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 { Action, Execution, Pack, StackStormApi } from './types';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
export class StackStormClient implements StackStormApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
constructor({
discoveryApi,
identityApi,
}: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.discoveryApi = discoveryApi;
this.identityApi = identityApi;
}
private async get<T = any>(input: string): Promise<T> {
const { token: idToken } = await this.identityApi.getCredentials();
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/stackstorm`;
const response = await fetch(`${apiUrl}${input}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
});
if (!response.ok) throw new Error(`Unable to get data: ${response.status}`);
return await response.json();
}
async getExecutions(limit?: number, offset?: number): Promise<Execution[]> {
const params = {
limit: limit?.toString() || '25',
offset: offset?.toString() || '0',
include_attributes:
'id,status,start_timestamp,action.ref,action.name,rule.ref',
parent: 'null',
};
const path = `/executions?${new URLSearchParams(params)}`;
return this.get<Execution[]>(path);
}
async getExecution(id: string): Promise<Execution> {
const path = `/executions/${id}`;
return this.get<Execution>(path);
}
async getPacks(): Promise<Pack[]> {
return this.get<Pack[]>('/packs');
}
async getActions(pack: string): Promise<Action[]> {
const params = {
include_attributes: 'id,ref,name,pack,description,runner_type',
pack: pack,
};
const path = `/actions?${new URLSearchParams(params)}`;
return this.get<Action[]>(path);
}
}
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright 2023 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 { stackStormApiRef } from './types';
export type {
Action,
Execution,
ExecutionLog,
Pack,
StackStormApi,
} from './types';
export { StackStormClient } from './StackStormClient';
+59
View File
@@ -0,0 +1,59 @@
/*
* Copyright 2023 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 { createApiRef } from '@backstage/core-plugin-api';
export const stackStormApiRef = createApiRef<StackStormApi>({
id: 'plugin.stackstorm.service',
});
export type Execution = {
id: string;
action: Action;
status: string;
start_timestamp: Date;
end_timestamp: Date;
result: object;
parameters: object;
elapsed_seconds: number;
log: ExecutionLog[];
};
export type ExecutionLog = {
status: string;
timestamp: Date;
};
export type Action = {
id: string;
name: string;
ref: string;
pack: string;
description: string;
runner_type: string;
};
export type Pack = {
ref: string;
description: string;
version: string;
};
export interface StackStormApi {
getExecutions(limit: number, offset: number): Promise<Execution[]>;
getExecution(id: string): Promise<Execution>;
getPacks(): Promise<Pack[]>;
getActions(pack: string): Promise<Action[]>;
}
@@ -0,0 +1,161 @@
/*
* Copyright 2023 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, { useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { Progress } from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import {
List,
ListItemText,
Collapse,
ListItem,
ListItemSecondaryAction,
ListItemIcon,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import ExpandMore from '@material-ui/icons/ExpandMore';
import ExpandLess from '@material-ui/icons/ExpandLess';
import { Alert } from '@material-ui/lab';
import { Action, Pack, stackStormApiRef } from '../../api';
const useStyles = makeStyles(theme => ({
root: {
width: '100%',
backgroundColor: theme.palette.background.paper,
},
actions: {
borderBottom: `2px solid ${theme.palette.divider}`,
},
nested: {
paddingLeft: theme.spacing(8),
paddingRight: theme.spacing(4),
},
icon: {
minWidth: '34px',
},
}));
type ActionItemsProps = {
pack: Pack;
};
export const ActionItems = ({ pack }: ActionItemsProps) => {
const classes = useStyles();
const st2 = useApi(stackStormApiRef);
const config = useApi(configApiRef);
const { value, loading, error } = useAsync(async (): Promise<Action[]> => {
const data = await st2.getActions(pack.ref);
return data;
}, []);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
return (
<List component="div" disablePadding className={classes.actions}>
{(value || []).map(a => {
return (
<ListItem
button
className={classes.nested}
onClick={() =>
window.open(
`${config.getString('stackstorm.webUrl')}/?#/actions/${a.ref}`,
'_blank',
)
}
>
<ListItemText primary={a.name} secondary={a.description} />
<ListItemSecondaryAction>{a.runner_type}</ListItemSecondaryAction>
</ListItem>
);
})}
</List>
);
};
type PackListItemProps = {
pack: Pack;
opened: boolean;
onClick: (ref: string) => any;
};
export const PackListItem = ({ pack, opened, onClick }: PackListItemProps) => {
const classes = useStyles();
return (
<>
<ListItem button onClick={() => onClick(pack.ref)}>
<ListItemIcon className={classes.icon}>
{opened ? <ExpandLess /> : <ExpandMore />}
</ListItemIcon>
<ListItemText primary={pack.ref} secondary={pack.description} />
<ListItemSecondaryAction>
version: {pack.version}
</ListItemSecondaryAction>
</ListItem>
<Collapse in={opened} timeout="auto" unmountOnExit>
<ActionItems pack={pack} />
</Collapse>
</>
);
};
export const ActionsList = () => {
const st2 = useApi(stackStormApiRef);
const classes = useStyles();
const [expanded, setExpanded] = useState<string[]>([]);
const onClick = (ref: string) => {
setExpanded(refs =>
refs.includes(ref) ? refs.filter(r => r !== ref) : refs.concat(ref),
);
};
const { value, loading, error } = useAsync(async (): Promise<Pack[]> => {
const data = await st2.getPacks();
return data;
}, []);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
return (
<List
component="nav"
aria-labelledby="nested-list-subheader"
className={classes.root}
>
{(value || []).map(p => {
return (
<PackListItem
pack={p}
opened={expanded.includes(p.ref)}
onClick={onClick}
/>
);
})}
</List>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 { ActionsList } from './ActionsList';
@@ -0,0 +1,165 @@
/*
* Copyright 2023 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 { CodeSnippet, Progress } from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { Execution, stackStormApiRef } from '../../api';
import useAsync from 'react-use/lib/useAsync';
import { Alert } from '@material-ui/lab';
import {
Button,
Card,
CardActions,
CardContent,
makeStyles,
Table,
TableBody,
TableCell,
TableRow,
Typography,
withStyles,
} from '@material-ui/core';
import { Status } from './Status';
const useStyles = makeStyles(theme => ({
table: {
maxWidth: '50%',
flex: 'i',
},
title: {
paddingTop: theme.spacing(2),
fontSize: 14,
fontWeight: 'bold',
textTransform: 'uppercase',
},
card: {
borderBottom: `2px solid ${theme.palette.divider}`,
},
}));
const THead = withStyles(() => ({
root: {
paddingLeft: 0,
},
}))(TableCell);
const TRow = withStyles(theme => ({
root: {
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.background.paper,
},
},
}))(TableRow);
const ExecutionCard = ({ e }: { e: Execution }) => {
const config = useApi(configApiRef);
const classes = useStyles();
const webUrl = `${config.getString('stackstorm.webUrl')}/?#/history/${e.id}`;
return (
<Card className={classes.card}>
<CardContent>
<Table className={classes.table} size="small">
<TableBody>
<TRow>
<THead component="th" scope="row">
Name
</THead>
<TableCell>{e.action.ref}</TableCell>
</TRow>
<TRow>
<THead component="th" scope="row">
Status
</THead>
<TableCell>
<Status status={e.status} />
</TableCell>
</TRow>
<TRow>
<THead component="th" scope="row">
Execution ID
</THead>
<TableCell>{e.id}</TableCell>
</TRow>
<TRow>
<THead component="th" scope="row">
Started
</THead>
<TableCell>{new Date(e.start_timestamp).toUTCString()}</TableCell>
</TRow>
<TRow>
<THead component="th" scope="row">
Finished
</THead>
<TableCell>{new Date(e.end_timestamp).toUTCString()}</TableCell>
</TRow>
<TRow>
<THead component="th" scope="row">
Execution Time
</THead>
<TableCell>{Math.round(e.elapsed_seconds)} s</TableCell>
</TRow>
<TRow>
<THead component="th" scope="row">
Runner
</THead>
<TableCell>{e.action.runner_type}</TableCell>
</TRow>
</TableBody>
</Table>
<Typography className={classes.title} gutterBottom>
Action Output
</Typography>
<CodeSnippet
text={JSON.stringify(e.result, null, 2)}
language="json"
customStyle={{ width: 800 }}
/>
<Typography className={classes.title} gutterBottom>
Action Input
</Typography>
<CodeSnippet
text={JSON.stringify(e.parameters, null, 2)}
language="json"
customStyle={{ width: 800 }}
/>
</CardContent>
<CardActions>
<Button size="small" href={webUrl} target="_blank">
View in ST2
</Button>
</CardActions>
</Card>
);
};
export const ExecutionPanel = ({ id }: { id: string }) => {
const st2 = useApi(stackStormApiRef);
const { value, loading, error } = useAsync(async (): Promise<Execution> => {
const data = await st2.getExecution(id);
return data;
}, []);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
return <ExecutionCard e={value!} />;
};
@@ -0,0 +1,130 @@
/*
* Copyright 2023 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, { useEffect, useState } from 'react';
import { Link, Table, TableColumn } from '@backstage/core-components';
import { useApi, configApiRef, errorApiRef } from '@backstage/core-plugin-api';
import { Execution, stackStormApiRef } from '../../api';
import { Status } from './Status';
import { ExecutionPanel } from './ExecutionPanel';
type DenseTableProps = {
executions: Execution[];
loading: boolean;
page: number;
pageSize: number;
onPageChange: (page: number) => void;
onRowsPerPageChange: (rows: number) => void;
};
export const DenseTable = ({
executions,
loading,
page,
pageSize,
onPageChange,
onRowsPerPageChange,
}: DenseTableProps) => {
const config = useApi(configApiRef);
const columns: TableColumn<Execution>[] = [
{
title: 'Status',
field: 'status',
render: e => <Status status={e.status} />,
},
{
title: 'Time',
field: 'start_timestamp',
render: e => `${new Date(e.start_timestamp).toUTCString()}`,
},
{ title: 'Name', field: 'action.ref' },
{
title: 'Execution ID',
field: 'id',
render: e => (
<Link
to={`${config.getString('stackstorm.webUrl')}/?#/history/${e.id}`}
>
{e.id}
</Link>
),
},
];
const count =
pageSize > executions.length
? (page + 1) * pageSize + executions.length - pageSize
: (page + 1) * pageSize + 1;
return (
<Table
title="Executions"
columns={columns}
data={executions}
page={page}
totalCount={count}
isLoading={loading}
options={{
paging: true,
search: false,
pageSize: pageSize,
padding: 'dense',
showFirstLastPageButtons: false,
}}
onPageChange={onPageChange}
onRowsPerPageChange={onRowsPerPageChange}
detailPanel={rowData => {
return <ExecutionPanel id={rowData.rowData.id} />;
}}
/>
);
};
export const ExecutionsTable = () => {
const st2 = useApi(stackStormApiRef);
const errorApi = useApi(errorApiRef);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [data, setData] = useState<Execution[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const getData = async () => {
setLoading(true);
await st2
.getExecutions(rowsPerPage, page * rowsPerPage)
.then(d => {
setData(d);
})
.catch(err => {
errorApi.post(err);
});
setLoading(false);
};
getData();
}, [errorApi, page, rowsPerPage, st2]);
return (
<DenseTable
page={page}
pageSize={rowsPerPage}
loading={loading}
executions={data}
onRowsPerPageChange={setRowsPerPage}
onPageChange={setPage}
/>
);
};
@@ -0,0 +1,57 @@
/*
* Copyright 2023 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 {
StatusOK,
StatusError,
StatusRunning,
StatusWarning,
} from '@backstage/core-components';
import React from 'react';
export const Status = ({ status }: { status: string | undefined }) => {
if (status === undefined) return null;
const st = status.toLocaleLowerCase('en-US');
switch (status) {
case 'succeeded':
case 'enabled':
case 'complete':
return (
<>
<StatusOK /> {st}
</>
);
case 'scheduled':
case 'running':
return (
<>
<StatusRunning /> {st}
</>
);
case 'failed':
case 'error':
return (
<>
<StatusError /> {st}
</>
);
default:
return (
<>
<StatusWarning /> {st}
</>
);
}
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 { ExecutionsTable } from './ExecutionsTable';
@@ -0,0 +1,59 @@
/*
* Copyright 2023 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 { Progress, Table, TableColumn } from '@backstage/core-components';
import { Alert } from '@material-ui/lab';
import useAsync from 'react-use/lib/useAsync';
import { useApi } from '@backstage/core-plugin-api';
import { Pack, stackStormApiRef } from '../../api';
type DenseTableProps = {
packs: Pack[];
};
export const DenseTable = ({ packs }: DenseTableProps) => {
const columns: TableColumn[] = [
{ title: 'Name', field: 'ref' },
{ title: 'Description', field: 'description' },
{ title: 'Version', field: 'version' },
];
return (
<Table
title="Packs"
options={{ search: true, paging: false }}
columns={columns}
data={packs}
/>
);
};
export const PacksTable = () => {
const st2 = useApi(stackStormApiRef);
const { value, loading, error } = useAsync(async (): Promise<Pack[]> => {
const data = await st2.getPacks();
return data;
}, []);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
return <DenseTable packs={value || []} />;
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 { PacksTable } from './PacksTable';
@@ -0,0 +1,38 @@
/*
* Copyright 2023 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 { StackStormHome } from './StackStormHome';
import { screen } from '@testing-library/react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { StackStormApi, stackStormApiRef } from '../../api';
describe('StackStormHome', () => {
const mockApi: jest.Mocked<StackStormApi> = {
getExecutions: jest.fn().mockResolvedValue([]),
getExecution: jest.fn().mockResolvedValue({}),
getPacks: jest.fn().mockResolvedValue([]),
getActions: jest.fn().mockResolvedValue([]),
} as any;
it('should render', async () => {
await renderInTestApp(
<TestApiProvider apis={[[stackStormApiRef, mockApi]]}>
<StackStormHome />
</TestApiProvider>,
);
expect(screen.getByText('Welcome to StackStorm!')).toBeInTheDocument();
});
});
@@ -0,0 +1,68 @@
/*
* Copyright 2023 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 { Grid, IconButton } from '@material-ui/core';
import {
Header,
Page,
HeaderLabel,
TabbedLayout,
Content,
} from '@backstage/core-components';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import { ExecutionsTable } from '../ExecutionsTable';
import { PacksTable } from '../PacksTable/PacksTable';
import { ActionsList } from '../ActionsList';
export const StackStormHome = () => (
<Page themeId="tool">
<Header title="Welcome to StackStorm!" subtitle="Event-driven automation">
<IconButton aria-label="Docs" href="https://docs.stackstorm.com/">
<LibraryBooks htmlColor="white" />
</IconButton>
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<TabbedLayout>
<TabbedLayout.Route path="/history" title="Executions">
<Content noPadding>
<Grid container spacing={3} direction="column">
<Grid>
<ExecutionsTable />
</Grid>
</Grid>
</Content>
</TabbedLayout.Route>
<TabbedLayout.Route path="/packs" title="Packs">
<Content noPadding>
<Grid container spacing={3} direction="column">
<Grid item>
<PacksTable />
</Grid>
</Grid>
</Content>
</TabbedLayout.Route>
<TabbedLayout.Route path="/actions" title="Actions">
<Content noPadding>
<Grid container spacing={3} direction="column">
<Grid item>
<ActionsList />
</Grid>
</Grid>
</Content>
</TabbedLayout.Route>
</TabbedLayout>
</Page>
);
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 { StackStormHome } from './StackStormHome';
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 { stackstormPlugin, StackstormPage } from './plugin';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2023 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 { stackstormPlugin } from './plugin';
describe('stackstorm', () => {
it('should export plugin', () => {
expect(stackstormPlugin).toBeDefined();
});
});
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright 2023 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 {
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { stackStormApiRef, StackStormClient } from './api';
import { rootRouteRef } from './routes';
export const stackstormPlugin = createPlugin({
id: 'stackstorm',
apis: [
createApiFactory({
api: stackStormApiRef,
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ discoveryApi, identityApi }) =>
new StackStormClient({
discoveryApi,
identityApi,
}),
}),
],
routes: {
root: rootRouteRef,
},
});
export const StackstormPage = stackstormPlugin.provide(
createRoutableExtension({
name: 'StackstormPage',
component: () =>
import('./components/StackStormHome').then(m => m.StackStormHome),
mountPoint: rootRouteRef,
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2023 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 { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'stackstorm',
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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 '@testing-library/jest-dom';
import 'cross-fetch/polyfill';