Merge pull request #16152 from pamelin/plugin-stackstorm
feat: Add StackStorm plugin
This commit is contained in:
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -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).
|
||||
@@ -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>;
|
||||
},
|
||||
{},
|
||||
{}
|
||||
>;
|
||||
```
|
||||
Vendored
+25
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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)}`;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user