diff --git a/.changeset/quick-tables-listen.md b/.changeset/quick-tables-listen.md new file mode 100644 index 0000000000..fab9ef7969 --- /dev/null +++ b/.changeset/quick-tables-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stackstorm': minor +--- + +Add StackStorm plugin to Backstage which is interfacing with StackStorm API. It allows users to view workflow executions, packs and actions. For installation and configuration instructions please view [README.md](https://github.com/backstage/backstage/tree/master/plugins/stackstorm/README.md) diff --git a/app-config.yaml b/app-config.yaml index dd72051189..c97adec5ef 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -118,6 +118,11 @@ proxy: headers: Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}' + '/stackstorm': + target: https://your.stackstorm.instance.com/api + headers: + St2-Api-Key: ${ST2_API_KEY} + organization: name: My Company @@ -441,5 +446,8 @@ apacheAirflow: gocd: baseUrl: https://your.gocd.instance.com +stackstorm: + webUrl: https://your.stackstorm.webui.instance.com + permission: enabled: true diff --git a/microsite/data/plugins/stackstorm.yaml b/microsite/data/plugins/stackstorm.yaml new file mode 100644 index 0000000000..2524ca538f --- /dev/null +++ b/microsite/data/plugins/stackstorm.yaml @@ -0,0 +1,14 @@ +--- +title: StackStorm +author: ExpediaGroup +authorUrl: https://github.com/ExpediaGroup +category: Automation +description: Manage StackStorm workflow executions from within Backstage. +documentation: https://github.com/backstage/backstage/tree/master/plugins/stackstorm +iconUrl: img/stackstorm.png +npmPackageName: '@backstage/plugin-stackstorm' +tags: + - stackstorm + - st2 + - automation + - workflow diff --git a/microsite/static/img/stackstorm.png b/microsite/static/img/stackstorm.png new file mode 100644 index 0000000000..f017906e07 Binary files /dev/null and b/microsite/static/img/stackstorm.png differ diff --git a/packages/app/package.json b/packages/app/package.json index 793e497996..822416ba09 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -60,6 +60,7 @@ "@backstage/plugin-sentry": "workspace:^", "@backstage/plugin-shortcuts": "workspace:^", "@backstage/plugin-stack-overflow": "workspace:^", + "@backstage/plugin-stackstorm": "workspace:^", "@backstage/plugin-tech-insights": "workspace:^", "@backstage/plugin-tech-radar": "workspace:^", "@backstage/plugin-techdocs": "workspace:^", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index f789328104..dfd2437b6b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -108,6 +108,7 @@ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common' import { PlaylistIndexPage } from '@backstage/plugin-playlist'; import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts'; import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card'; +import { StackstormPage } from '@backstage/plugin-stackstorm'; const app = createApp({ apis, @@ -281,6 +282,7 @@ const routes = ( } /> } /> } /> + } /> ); diff --git a/plugins/stackstorm/.eslintrc.js b/plugins/stackstorm/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/stackstorm/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/stackstorm/README.md b/plugins/stackstorm/README.md new file mode 100644 index 0000000000..0d5caeaca2 --- /dev/null +++ b/plugins/stackstorm/README.md @@ -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 = ( + + {/* ...other routes */} + } /> + +``` + +### 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). diff --git a/plugins/stackstorm/api-report.md b/plugins/stackstorm/api-report.md new file mode 100644 index 0000000000..3e3d79fe27 --- /dev/null +++ b/plugins/stackstorm/api-report.md @@ -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 +/// + +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; + }, + {}, + {} +>; +``` diff --git a/plugins/stackstorm/config.d.ts b/plugins/stackstorm/config.d.ts new file mode 100644 index 0000000000..bf7b56823a --- /dev/null +++ b/plugins/stackstorm/config.d.ts @@ -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; + }; +} diff --git a/plugins/stackstorm/dev/index.tsx b/plugins/stackstorm/dev/index.tsx new file mode 100644 index 0000000000..33bd89adda --- /dev/null +++ b/plugins/stackstorm/dev/index.tsx @@ -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: , + title: 'Root Page', + path: '/stackstorm', + }) + .render(); diff --git a/plugins/stackstorm/package.json b/plugins/stackstorm/package.json new file mode 100644 index 0000000000..c5b266782e --- /dev/null +++ b/plugins/stackstorm/package.json @@ -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" +} diff --git a/plugins/stackstorm/src/api/StackstormClient.test.ts b/plugins/stackstorm/src/api/StackstormClient.test.ts new file mode 100644 index 0000000000..9757bde2af --- /dev/null +++ b/plugins/stackstorm/src/api/StackstormClient.test.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', + ); + }); +}); diff --git a/plugins/stackstorm/src/api/StackstormClient.ts b/plugins/stackstorm/src/api/StackstormClient.ts new file mode 100644 index 0000000000..495384e8a5 --- /dev/null +++ b/plugins/stackstorm/src/api/StackstormClient.ts @@ -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(input: string): Promise { + 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 { + 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(path); + } + + async getExecution(id: string): Promise { + const path = `/executions/${encodeURIComponent(id)}`; + return this.get(path); + } + + async getPacks(): Promise { + return this.get('/packs'); + } + + async getActions(pack: string): Promise { + const params = { + include_attributes: 'id,ref,name,pack,description,runner_type', + pack: pack, + }; + const path = `/actions?${new URLSearchParams(params)}`; + return this.get(path); + } + + getExecutionHistoryUrl(id: string): string { + return `${this.webUrl}/?#/history/${encodeURIComponent(id)}`; + } + + getActionUrl(ref: string): string { + return `${this.webUrl}/?#/actions/${encodeURIComponent(ref)}`; + } +} diff --git a/plugins/stackstorm/src/api/index.ts b/plugins/stackstorm/src/api/index.ts new file mode 100644 index 0000000000..0ce652e27b --- /dev/null +++ b/plugins/stackstorm/src/api/index.ts @@ -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'; diff --git a/plugins/stackstorm/src/api/types.ts b/plugins/stackstorm/src/api/types.ts new file mode 100644 index 0000000000..6ebf3d67a6 --- /dev/null +++ b/plugins/stackstorm/src/api/types.ts @@ -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({ + 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; + getExecution(id: string): Promise; + getPacks(): Promise; + getActions(pack: string): Promise; + getExecutionHistoryUrl(id: string): string; + getActionUrl(ref: string): string; +} diff --git a/plugins/stackstorm/src/components/ActionsList/ActionsList.test.tsx b/plugins/stackstorm/src/components/ActionsList/ActionsList.test.tsx new file mode 100644 index 0000000000..2a65db427a --- /dev/null +++ b/plugins/stackstorm/src/components/ActionsList/ActionsList.test.tsx @@ -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 = { + 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( + + + , + ); + + packs.forEach(p => { + expect(getByText(p.ref)).toBeInTheDocument(); + expect(getByText(p.description)).toBeInTheDocument(); + }); + }); + + it('should render all pack actions', async () => { + const { getByText } = await renderInTestApp( + + {}} /> + , + ); + + actions.forEach(a => { + expect(getByText(a.name)).toBeInTheDocument(); + expect(getByText(a.runner_type)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/stackstorm/src/components/ActionsList/ActionsList.tsx b/plugins/stackstorm/src/components/ActionsList/ActionsList.tsx new file mode 100644 index 0000000000..1080f67373 --- /dev/null +++ b/plugins/stackstorm/src/components/ActionsList/ActionsList.tsx @@ -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 => { + const data = await st2.getActions(pack.ref); + return data; + }, []); + + if (loading) { + return ; + } else if (error) { + return ; + } + + return ( + + {(value || []).map(a => { + return ( + + + {a.runner_type} + + ); + })} + + ); +}; + +type PackListItemProps = { + pack: Pack; + opened: boolean; + onClick: (ref: string) => any; +}; + +export const PackListItem = ({ pack, opened, onClick }: PackListItemProps) => { + const classes = useStyles(); + + return ( + <> + onClick(pack.ref)}> + + {opened ? : } + + + + version: {pack.version} + + + + + + + ); +}; + +export const ActionsList = () => { + const st2 = useApi(stackstormApiRef); + + const classes = useStyles(); + const [expanded, setExpanded] = useState([]); + + const onClick = (ref: string) => { + setExpanded(refs => + refs.includes(ref) ? refs.filter(r => r !== ref) : refs.concat(ref), + ); + }; + + const { value, loading, error } = useAsync(async (): Promise => { + const data = await st2.getPacks(); + return data; + }, []); + + if (loading) { + return ; + } else if (error) { + return ; + } + + return ( + + {(value || []).map(p => { + return ( + + ); + })} + + ); +}; diff --git a/plugins/stackstorm/src/components/ActionsList/index.ts b/plugins/stackstorm/src/components/ActionsList/index.ts new file mode 100644 index 0000000000..1606d1be3d --- /dev/null +++ b/plugins/stackstorm/src/components/ActionsList/index.ts @@ -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'; diff --git a/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.test.tsx b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.test.tsx new file mode 100644 index 0000000000..53930d339f --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.test.tsx @@ -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 = { + 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( + + + , + ); + + expect(getByText(execution.id)).toBeInTheDocument(); + expect(getByText(execution.status)).toBeInTheDocument(); + expect(getByText('"hello world!"')).toBeInTheDocument(); + expect(getByText('"my funky partameter"')).toBeInTheDocument(); + }); +}); diff --git a/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.tsx b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.tsx new file mode 100644 index 0000000000..8a55c31246 --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionPanel.tsx @@ -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 ( + + + + + + + Name + + {e.action.ref} + + + + Status + + + + + + + + Execution ID + + {e.id} + + + + Started + + {new Date(e.start_timestamp).toUTCString()} + + + + Finished + + {new Date(e.end_timestamp).toUTCString()} + + + + Execution Time + + {Math.round(e.elapsed_seconds)} s + + + + Runner + + {e.action.runner_type} + + +
+ + Action Output + + + + Action Input + + +
+ + + +
+ ); +}; + +export const ExecutionPanel = ({ id }: { id: string }) => { + const st2 = useApi(stackstormApiRef); + + const { value, loading, error } = useAsync(async (): Promise => { + const data = await st2.getExecution(id); + return data; + }, []); + + if (loading) { + return ; + } else if (error) { + return ; + } + + return ; +}; diff --git a/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.test.tsx b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.test.tsx new file mode 100644 index 0000000000..6727f3cc56 --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.test.tsx @@ -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 = { + 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( + + + , + ); + + executions.forEach(e => { + expect(getByText(e.id)).toBeInTheDocument(); + expect(getByText(e.action.ref)).toBeInTheDocument(); + expect(getByText(e.status)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.tsx b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.tsx new file mode 100644 index 0000000000..f034cb64e8 --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/ExecutionsTable.tsx @@ -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[] = [ + { + title: 'Status', + field: 'status', + render: e => , + }, + { + 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 => {e.id}, + }, + ]; + + const count = + pageSize > executions.length + ? (page + 1) * pageSize + executions.length - pageSize + : (page + 1) * pageSize + 1; + + return ( + { + return ; + }} + /> + ); +}; + +export const ExecutionsTable = () => { + const st2 = useApi(stackstormApiRef); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + + const { value, loading, error } = useAsync(async (): Promise => { + const data = await st2.getExecutions(rowsPerPage, page * rowsPerPage); + return data; + }, [page, rowsPerPage, st2]); + + if (error) { + return ; + } + + return ( + + ); +}; diff --git a/plugins/stackstorm/src/components/ExecutionsTable/Status.tsx b/plugins/stackstorm/src/components/ExecutionsTable/Status.tsx new file mode 100644 index 0000000000..99a94df2c8 --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/Status.tsx @@ -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 ( + <> + {st} + + ); + case 'scheduled': + case 'running': + return ( + <> + {st} + + ); + case 'failed': + case 'error': + return ( + <> + {st} + + ); + default: + return ( + <> + {st} + + ); + } +}; diff --git a/plugins/stackstorm/src/components/ExecutionsTable/index.ts b/plugins/stackstorm/src/components/ExecutionsTable/index.ts new file mode 100644 index 0000000000..402d8e557d --- /dev/null +++ b/plugins/stackstorm/src/components/ExecutionsTable/index.ts @@ -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'; diff --git a/plugins/stackstorm/src/components/PacksTable/PacksTable.test.tsx b/plugins/stackstorm/src/components/PacksTable/PacksTable.test.tsx new file mode 100644 index 0000000000..9de17e2f14 --- /dev/null +++ b/plugins/stackstorm/src/components/PacksTable/PacksTable.test.tsx @@ -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 = { + getPacks: jest.fn().mockResolvedValue(packs), + } as any; + + it('should render all packs', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + + packs.forEach(p => { + expect(getByText(p.ref)).toBeInTheDocument(); + expect(getByText(p.description)).toBeInTheDocument(); + expect(getByText(p.version)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/stackstorm/src/components/PacksTable/PacksTable.tsx b/plugins/stackstorm/src/components/PacksTable/PacksTable.tsx new file mode 100644 index 0000000000..2167200e8e --- /dev/null +++ b/plugins/stackstorm/src/components/PacksTable/PacksTable.tsx @@ -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 ( +
+ ); +}; + +export const PacksTable = () => { + const st2 = useApi(stackstormApiRef); + + const { value, loading, error } = useAsync(async (): Promise => { + const data = await st2.getPacks(); + return data; + }, []); + + if (loading) { + return ; + } else if (error) { + return ; + } + + return ; +}; diff --git a/plugins/stackstorm/src/components/PacksTable/index.ts b/plugins/stackstorm/src/components/PacksTable/index.ts new file mode 100644 index 0000000000..378e32a5d5 --- /dev/null +++ b/plugins/stackstorm/src/components/PacksTable/index.ts @@ -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'; diff --git a/plugins/stackstorm/src/components/StackstormHome/StackstormHome.test.tsx b/plugins/stackstorm/src/components/StackstormHome/StackstormHome.test.tsx new file mode 100644 index 0000000000..897cec61c1 --- /dev/null +++ b/plugins/stackstorm/src/components/StackstormHome/StackstormHome.test.tsx @@ -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 = { + getExecutions: jest.fn().mockResolvedValue([]), + getExecution: jest.fn().mockResolvedValue({}), + getPacks: jest.fn().mockResolvedValue([]), + getActions: jest.fn().mockResolvedValue([]), + } as any; + + it('should render', async () => { + await renderInTestApp( + + + , + ); + expect(screen.getByText('Welcome to StackStorm!')).toBeInTheDocument(); + }); +}); diff --git a/plugins/stackstorm/src/components/StackstormHome/StackstormHome.tsx b/plugins/stackstorm/src/components/StackstormHome/StackstormHome.tsx new file mode 100644 index 0000000000..d96d45e82d --- /dev/null +++ b/plugins/stackstorm/src/components/StackstormHome/StackstormHome.tsx @@ -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 = () => ( + +
+ + + + +
+ + + + + + + + + + + + + + + + + +
+); diff --git a/plugins/stackstorm/src/components/StackstormHome/index.ts b/plugins/stackstorm/src/components/StackstormHome/index.ts new file mode 100644 index 0000000000..2063d21f34 --- /dev/null +++ b/plugins/stackstorm/src/components/StackstormHome/index.ts @@ -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'; diff --git a/plugins/stackstorm/src/index.ts b/plugins/stackstorm/src/index.ts new file mode 100644 index 0000000000..b64ae207e4 --- /dev/null +++ b/plugins/stackstorm/src/index.ts @@ -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'; diff --git a/plugins/stackstorm/src/plugin.test.ts b/plugins/stackstorm/src/plugin.test.ts new file mode 100644 index 0000000000..9ef2f438c1 --- /dev/null +++ b/plugins/stackstorm/src/plugin.test.ts @@ -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(); + }); +}); diff --git a/plugins/stackstorm/src/plugin.ts b/plugins/stackstorm/src/plugin.ts new file mode 100644 index 0000000000..be4b9c065c --- /dev/null +++ b/plugins/stackstorm/src/plugin.ts @@ -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, + }), +); diff --git a/plugins/stackstorm/src/routes.ts b/plugins/stackstorm/src/routes.ts new file mode 100644 index 0000000000..90cf7760bc --- /dev/null +++ b/plugins/stackstorm/src/routes.ts @@ -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', +}); diff --git a/plugins/stackstorm/src/setupTests.ts b/plugins/stackstorm/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/stackstorm/src/setupTests.ts @@ -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'; diff --git a/yarn.lock b/yarn.lock index 7d905a0873..0b2b2f350e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8008,6 +8008,33 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-stackstorm@workspace:^, @backstage/plugin-stackstorm@workspace:plugins/stackstorm": + version: 0.0.0-use.local + resolution: "@backstage/plugin-stackstorm@workspace:plugins/stackstorm" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.57 + "@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 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-tech-insights-backend-module-jsonfc@workspace:^, @backstage/plugin-tech-insights-backend-module-jsonfc@workspace:plugins/tech-insights-backend-module-jsonfc": version: 0.0.0-use.local resolution: "@backstage/plugin-tech-insights-backend-module-jsonfc@workspace:plugins/tech-insights-backend-module-jsonfc" @@ -22407,6 +22434,7 @@ __metadata: "@backstage/plugin-sentry": "workspace:^" "@backstage/plugin-shortcuts": "workspace:^" "@backstage/plugin-stack-overflow": "workspace:^" + "@backstage/plugin-stackstorm": "workspace:^" "@backstage/plugin-tech-insights": "workspace:^" "@backstage/plugin-tech-radar": "workspace:^" "@backstage/plugin-techdocs": "workspace:^"