Merge pull request #16152 from pamelin/plugin-stackstorm

feat: Add StackStorm plugin
This commit is contained in:
Fredrik Adelöw
2023-02-17 15:04:10 +01:00
committed by GitHub
37 changed files with 1864 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+61
View File
@@ -0,0 +1,61 @@
# 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
# From your Backstage root directory
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).
+22
View File
@@ -0,0 +1,22 @@
## API Report File for "@backstage/plugin-stackstorm"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { RouteRef } from '@backstage/core-plugin-api';
// @public
export const StackstormPage: () => JSX.Element;
// @public
export const stackstormPlugin: BackstagePlugin<
{
root: RouteRef<undefined>;
},
{},
{}
>;
```
+25
View File
@@ -0,0 +1,25 @@
/*
* 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 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();
+66
View File
@@ -0,0 +1,66 @@
{
"name": "@backstage/plugin-stackstorm",
"description": "A Backstage plugin that integrates towards StackStorm",
"version": "0.0.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/errors": "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,226 @@
/*
* 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 { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api';
import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { StackstormClient } from './StackstormClient';
import { Action, Execution, Pack } from './types';
const server = setupServer();
const executions: Execution[] = [
{
id: '63dcac3e18ba00e09e7bb3b6',
action: {
name: 'post_message',
ref: 'chatops.post_message',
description: 'Post a message to stream for chatops',
pack: 'chatops',
runner_type: 'announcement',
id: '62fe101b11935b6aaff4ff92',
},
status: 'succeeded',
start_timestamp: new Date().toISOString(),
end_timestamp: new Date().toISOString(),
result: {},
parameters: {},
elapsed_seconds: 2.2,
log: [],
},
{
id: '63dcac3e18ba00e09e7bb3b6',
action: {
name: 'post_result',
ref: 'chatops.post_result',
description: 'Post an execution result to stream for chatops',
pack: 'chatops',
runner_type: 'orquesta',
id: '62fe101b11935b6aaff4ff93',
},
status: 'succeeded',
start_timestamp: new Date().toISOString(),
end_timestamp: new Date().toISOString(),
result: {},
parameters: {},
elapsed_seconds: 18.5,
log: [],
},
{
id: '63dcac3c9e0b4fe98f46becc',
action: {
name: 'run',
ref: 'shell.run',
description: 'Run shell script',
pack: 'shell',
runner_type: 'shell',
id: '63736caac3d8557c4d61883a',
},
status: 'failed',
start_timestamp: new Date().toISOString(),
end_timestamp: new Date().toISOString(),
result: {},
parameters: {},
elapsed_seconds: 5.0,
log: [],
},
];
const executionWithDetails: Execution = {
id: '63dcac3e18ba00e09e7bb3b6',
action: {
name: 'post_message',
ref: 'chatops.post_message',
description: 'Post a message to stream for chatops',
pack: 'chatops',
runner_type: 'announcement',
id: '62fe101b11935b6aaff4ff92',
},
status: 'succeeded',
start_timestamp: new Date().toISOString(),
end_timestamp: new Date().toISOString(),
result: {
output: {
result: 'hello',
},
},
parameters: {
var: 'val',
},
elapsed_seconds: 2.2,
log: [
{ status: 'requested', timestamp: new Date().toISOString() },
{ status: 'succeeded', timestamp: new Date().toISOString() },
],
};
const packs: Pack[] = [
{
ref: 'chatops',
description: 'ChatOps integration pack',
version: '3.7.0',
},
{
ref: 'core',
description: 'Basic core actions.',
version: '3.7.0',
},
];
const actions: Action[] = [
{
id: '62fe101b11935b6aaff4ff96',
name: 'announcement',
ref: 'core.announcement',
pack: 'core',
description:
'Action that broadcasts the announcement to all stream consumers.',
runner_type: 'announcement',
},
{
id: '62fe101b11935b6aaff4ff97',
name: 'echo',
ref: 'core.echo',
pack: 'core',
description:
'Action that executes the Linux echo command on the localhost.',
runner_type: 'local-shell-cmd',
},
];
describe('StackstormClient', () => {
setupRequestMockHandlers(server);
const mockBaseUrl = 'http://backstage:9191/api/proxy';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
let client: StackstormClient;
const setupHandlers = () => {
server.use(
rest.get(`${mockBaseUrl}/stackstorm/executions`, (req, res, ctx) => {
const limit = req.url.searchParams.get('limit')
? Number(req.url.searchParams.get('limit'))
: executions.length;
const offset = req.url.searchParams.get('offset')
? Number(req.url.searchParams.get('offset'))
: 0;
return res(ctx.json(executions.slice(offset, offset + limit)));
}),
rest.get(
`${mockBaseUrl}/stackstorm/executions/63dcac3e18ba00e09e7bb3b6`,
(_req, res, ctx) => {
return res(ctx.json(executionWithDetails));
},
),
rest.get(`${mockBaseUrl}/stackstorm/packs`, (_req, res, ctx) => {
return res(ctx.json(packs));
}),
rest.get(`${mockBaseUrl}/stackstorm/actions`, (req, res, ctx) => {
const name = req.url.searchParams.get('pack');
return res(ctx.json(name === 'core' ? actions : []));
}),
);
};
beforeEach(() => {
setupHandlers();
client = StackstormClient.fromConfig(
new ConfigReader({
stackstorm: {
webUrl: 'http://stackstorm.example.com:8080',
},
}),
{
discoveryApi: discoveryApi,
fetchApi: new MockFetchApi(),
},
);
});
it('getExecutions should return executions with emulated pagination', async () => {
const got = await client.getExecutions(2, 1);
expect(got.length).toEqual(2);
expect(got).toMatchObject(executions.slice(1, 3));
});
it('getExecution should return one execution', async () => {
const got = await client.getExecution('63dcac3e18ba00e09e7bb3b6');
expect(got).toMatchObject(executionWithDetails);
});
it('getPacks should return list of all packs', async () => {
const got = await client.getPacks();
expect(got.length).toEqual(packs.length);
expect(got).toMatchObject(packs);
});
it('getActions should return list of actions', async () => {
const got = await client.getActions('core');
expect(got).toMatchObject(actions);
});
it('getExecutionHistoryUrl should return webUrl for executions', async () => {
const got = client.getExecutionHistoryUrl('123abc');
expect(got).toEqual('http://stackstorm.example.com:8080/?#/history/123abc');
});
it('getActionUrl should return webUrl for action', async () => {
const got = client.getActionUrl('core.shell');
expect(got).toEqual(
'http://stackstorm.example.com:8080/?#/actions/core.shell',
);
});
});
@@ -0,0 +1,102 @@
/*
* 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 { ConfigApi, DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
export class StackstormClient implements StackstormApi {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
private readonly webUrl: string;
private constructor({
discoveryApi,
fetchApi,
webUrl,
}: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
webUrl: string;
}) {
this.discoveryApi = discoveryApi;
this.fetchApi = fetchApi;
this.webUrl = webUrl;
}
static fromConfig(
config: ConfigApi,
dependencies: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
},
): StackstormClient {
return new StackstormClient({
discoveryApi: dependencies.discoveryApi,
fetchApi: dependencies.fetchApi,
webUrl: config.getString('stackstorm.webUrl'),
});
}
private async get<T = any>(input: string): Promise<T> {
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/stackstorm`;
const response = await this.fetchApi.fetch(`${apiUrl}${input}`, {
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) throw await ResponseError.fromResponse(response);
return (await response.json()) as T;
}
async getExecutions(limit?: number, offset?: number): Promise<Execution[]> {
const params = {
limit: limit?.toString() || '10',
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/${encodeURIComponent(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);
}
getExecutionHistoryUrl(id: string): string {
return `${this.webUrl}/?#/history/${encodeURIComponent(id)}`;
}
getActionUrl(ref: string): string {
return `${this.webUrl}/?#/actions/${encodeURIComponent(ref)}`;
}
}
+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';
+61
View File
@@ -0,0 +1,61 @@
/*
* 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: string;
end_timestamp: string;
result: object;
parameters: object;
elapsed_seconds: number;
log: ExecutionLog[];
};
export type ExecutionLog = {
status: string;
timestamp: string;
};
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[]>;
getExecutionHistoryUrl(id: string): string;
getActionUrl(ref: string): string;
}
@@ -0,0 +1,91 @@
/*
* 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import React from 'react';
import { Action, Pack, StackstormApi, stackstormApiRef } from '../../api';
import { ActionsList, PackListItem } from './ActionsList';
const packs: Pack[] = [
{
ref: 'chatops',
description: 'ChatOps integration pack',
version: '3.7.0',
},
{
ref: 'core',
description: 'Basic core actions.',
version: '3.7.1',
},
];
const actions: Action[] = [
{
id: '62fe101b11935b6aaff4ff96',
name: 'announcement',
ref: 'core.announcement',
pack: 'core',
description:
'Action that broadcasts the announcement to all stream consumers.',
runner_type: 'broadcast',
},
{
id: '62fe101b11935b6aaff4ff97',
name: 'echo',
ref: 'core.echo',
pack: 'core',
description:
'Action that executes the Linux echo command on the localhost.',
runner_type: 'local-shell-cmd',
},
];
describe('ActionsList', () => {
const mockApi: jest.Mocked<StackstormApi> = {
getPacks: jest.fn().mockResolvedValue(packs),
getActions: jest.fn().mockResolvedValue(actions),
getActionUrl: jest
.fn()
.mockResolvedValue(
'http://stackstorm.example.com:8080/?#/actions/core.action',
),
} as any;
it('should render all packs', async () => {
const { getByText } = await renderInTestApp(
<TestApiProvider apis={[[stackstormApiRef, mockApi]]}>
<ActionsList />
</TestApiProvider>,
);
packs.forEach(p => {
expect(getByText(p.ref)).toBeInTheDocument();
expect(getByText(p.description)).toBeInTheDocument();
});
});
it('should render all pack actions', async () => {
const { getByText } = await renderInTestApp(
<TestApiProvider apis={[[stackstormApiRef, mockApi]]}>
<PackListItem pack={packs[1]} opened onClick={() => {}} />
</TestApiProvider>,
);
actions.forEach(a => {
expect(getByText(a.name)).toBeInTheDocument();
expect(getByText(a.runner_type)).toBeInTheDocument();
});
});
});
@@ -0,0 +1,159 @@
/*
* 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 { Link, Progress, ResponseErrorPanel } from '@backstage/core-components';
import { useApi } 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 { 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 { value, loading, error } = useAsync(async (): Promise<Action[]> => {
const data = await st2.getActions(pack.ref);
return data;
}, []);
if (loading) {
return <Progress />;
} else if (error) {
return <ResponseErrorPanel error={error} />;
}
return (
<List component="div" disablePadding className={classes.actions}>
{(value || []).map(a => {
return (
<ListItem
key={a.ref}
component={Link}
to={st2.getActionUrl(a.ref)}
className={classes.nested}
underline="none"
color="inherit"
button
>
<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 <ResponseErrorPanel error={error} />;
}
return (
<List
component="nav"
aria-labelledby="nested-list-subheader"
className={classes.root}
>
{(value || []).map(p => {
return (
<PackListItem
key={p.ref}
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,71 @@
/*
* 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import React from 'react';
import { Execution, StackstormApi, stackstormApiRef } from '../../api';
import { ExecutionPanel } from './ExecutionPanel';
const execution: Execution = {
id: '63dcac3e18ba00e09e7bb3b6',
action: {
name: 'post_message',
ref: 'chatops.post_message',
description: 'Post a message to stream for chatops',
pack: 'chatops',
runner_type: 'announcement',
id: '62fe101b11935b6aaff4ff92',
},
status: 'succeeded',
start_timestamp: new Date().toISOString(),
end_timestamp: new Date().toISOString(),
result: {
output: {
result: 'hello world!',
},
},
parameters: {
var: 'my funky partameter',
},
elapsed_seconds: 2.2,
log: [
{ status: 'requested', timestamp: new Date().toISOString() },
{ status: 'succeeded', timestamp: new Date().toISOString() },
],
};
describe('ExecutionPanel', () => {
const mockApi: jest.Mocked<StackstormApi> = {
getExecution: jest.fn().mockResolvedValue(execution),
getExecutionHistoryUrl: jest
.fn()
.mockResolvedValue(
'http://stackstorm.example.com:8080/?#/history/63dcac3e18ba00e09e7bb3b6',
),
} as any;
it('should render execution details', async () => {
const { getByText } = await renderInTestApp(
<TestApiProvider apis={[[stackstormApiRef, mockApi]]}>
<ExecutionPanel id="63dcac3e18ba00e09e7bb3b6" />
</TestApiProvider>,
);
expect(getByText(execution.id)).toBeInTheDocument();
expect(getByText(execution.status)).toBeInTheDocument();
expect(getByText('"hello world!"')).toBeInTheDocument();
expect(getByText('"my funky partameter"')).toBeInTheDocument();
});
});
@@ -0,0 +1,170 @@
/*
* 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,
ResponseErrorPanel,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { Execution, stackstormApiRef } from '../../api';
import useAsync from 'react-use/lib/useAsync';
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 st2 = useApi(stackstormApiRef);
const classes = useStyles();
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={`${st2.getExecutionHistoryUrl(e.id)}`}
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 <ResponseErrorPanel error={error} />;
}
return <ExecutionCard e={value!} />;
};
@@ -0,0 +1,101 @@
/*
* 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import React from 'react';
import { Execution, StackstormApi, stackstormApiRef } from '../../api';
import { ExecutionsTable } from './ExecutionsTable';
const executions: Execution[] = [
{
id: '63dcac3e18ba00e09e7bb3b6',
action: {
name: 'post_message',
ref: 'chatops.post_message',
description: 'Post a message to stream for chatops',
pack: 'chatops',
runner_type: 'announcement',
id: '62fe101b11935b6aaff4ff92',
},
status: 'processing',
start_timestamp: new Date().toISOString(),
end_timestamp: new Date().toISOString(),
result: {},
parameters: {},
elapsed_seconds: 2.2,
log: [],
},
{
id: '62fe101b11935b6aaff4ff93',
action: {
name: 'post_result',
ref: 'chatops.post_result',
description: 'Post an execution result to stream for chatops',
pack: 'chatops',
runner_type: 'orquesta',
id: '62fe101b11935b6aaff4ff93',
},
status: 'succeeded',
start_timestamp: new Date().toISOString(),
end_timestamp: new Date().toISOString(),
result: {},
parameters: {},
elapsed_seconds: 18.5,
log: [],
},
{
id: '63dcac3c9e0b4fe98f46becc',
action: {
name: 'run',
ref: 'shell.run',
description: 'Run shell script',
pack: 'shell',
runner_type: 'shell',
id: '63736caac3d8557c4d61883a',
},
status: 'failed',
start_timestamp: new Date().toISOString(),
end_timestamp: new Date().toISOString(),
result: {},
parameters: {},
elapsed_seconds: 5.0,
log: [],
},
];
describe('ExecutionsTable', () => {
const mockApi: jest.Mocked<StackstormApi> = {
getExecutions: jest.fn().mockResolvedValue(executions),
getExecutionHistoryUrl: jest
.fn()
.mockResolvedValue(
'http://stackstorm.example.com:8080/?#/history/123abc',
),
} as any;
it('should render all executions', async () => {
const { getByText } = await renderInTestApp(
<TestApiProvider apis={[[stackstormApiRef, mockApi]]}>
<ExecutionsTable />
</TestApiProvider>,
);
executions.forEach(e => {
expect(getByText(e.id)).toBeInTheDocument();
expect(getByText(e.action.ref)).toBeInTheDocument();
expect(getByText(e.status)).toBeInTheDocument();
});
});
});
@@ -0,0 +1,120 @@
/*
* 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 {
Link,
ResponseErrorPanel,
Table,
TableColumn,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { Execution, stackstormApiRef } from '../../api';
import { Status } from './Status';
import { ExecutionPanel } from './ExecutionPanel';
import useAsync from 'react-use/lib/useAsync';
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 st2 = useApi(stackstormApiRef);
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={st2.getExecutionHistoryUrl(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 [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const { value, loading, error } = useAsync(async (): Promise<Execution[]> => {
const data = await st2.getExecutions(rowsPerPage, page * rowsPerPage);
return data;
}, [page, rowsPerPage, st2]);
if (error) {
return <ResponseErrorPanel error={error} />;
}
return (
<DenseTable
page={page}
pageSize={rowsPerPage}
loading={loading}
executions={value || []}
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,52 @@
/*
* 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import React from 'react';
import { Pack, StackstormApi, stackstormApiRef } from '../../api';
import { PacksTable } from './PacksTable';
const packs: Pack[] = [
{
ref: 'chatops',
description: 'ChatOps integration pack',
version: '3.7.0',
},
{
ref: 'core',
description: 'Basic core actions.',
version: '3.7.1',
},
];
describe('PacksTable', () => {
const mockApi: jest.Mocked<StackstormApi> = {
getPacks: jest.fn().mockResolvedValue(packs),
} as any;
it('should render all packs', async () => {
const { getByText } = await renderInTestApp(
<TestApiProvider apis={[[stackstormApiRef, mockApi]]}>
<PacksTable />
</TestApiProvider>,
);
packs.forEach(p => {
expect(getByText(p.ref)).toBeInTheDocument();
expect(getByText(p.description)).toBeInTheDocument();
expect(getByText(p.version)).toBeInTheDocument();
});
});
});
@@ -0,0 +1,63 @@
/*
* 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,
ResponseErrorPanel,
Table,
TableColumn,
} from '@backstage/core-components';
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 <ResponseErrorPanel error={error} />;
}
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,56 @@
/*
* 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 { 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>
<ExecutionsTable />
</Content>
</TabbedLayout.Route>
<TabbedLayout.Route path="/packs" title="Packs">
<Content noPadding>
<PacksTable />
</Content>
</TabbedLayout.Route>
<TabbedLayout.Route path="/actions" title="Actions">
<Content noPadding>
<ActionsList />
</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';
+23
View File
@@ -0,0 +1,23 @@
/*
* 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.
*/
/**
* A Backstage plugin that integrates towards StackStorm
*
* @packageDocumentation
*/
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();
});
});
+67
View File
@@ -0,0 +1,67 @@
/*
* 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 {
configApiRef,
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import { stackstormApiRef, StackstormClient } from './api';
import { rootRouteRef } from './routes';
/**
* The Backstage plugin that holds stackstorm specific components
*
* @public
*/
export const stackstormPlugin = createPlugin({
id: 'stackstorm',
apis: [
createApiFactory({
api: stackstormApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
},
factory: ({ configApi, discoveryApi, fetchApi }) =>
StackstormClient.fromConfig(configApi, {
discoveryApi,
fetchApi,
}),
}),
],
routes: {
root: rootRouteRef,
},
});
/**
* A component to display a stackstorm home page
*
* @public
*/
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';