@backstage-community/plugin-multi-source-security-viewer 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +253 -0
  3. package/dist/api/github.esm.js +114 -0
  4. package/dist/api/github.esm.js.map +1 -0
  5. package/dist/api/gitlab.esm.js +84 -0
  6. package/dist/api/gitlab.esm.js.map +1 -0
  7. package/dist/api/jenkins.esm.js +119 -0
  8. package/dist/api/jenkins.esm.js.map +1 -0
  9. package/dist/components/DialogLauncher/DialogLauncher.esm.js +27 -0
  10. package/dist/components/DialogLauncher/DialogLauncher.esm.js.map +1 -0
  11. package/dist/components/EmptyState/EmptyState.esm.js +51 -0
  12. package/dist/components/EmptyState/EmptyState.esm.js.map +1 -0
  13. package/dist/components/EmptyState/EmptyStateNoData.esm.js +15 -0
  14. package/dist/components/EmptyState/EmptyStateNoData.esm.js.map +1 -0
  15. package/dist/components/EmptyState/EmptyStateNoMatch.esm.js +16 -0
  16. package/dist/components/EmptyState/EmptyStateNoMatch.esm.js.map +1 -0
  17. package/dist/components/EmptyState/EmptyStateSpinner.esm.js +19 -0
  18. package/dist/components/EmptyState/EmptyStateSpinner.esm.js.map +1 -0
  19. package/dist/components/Icons/CriticalIcon.esm.js +18 -0
  20. package/dist/components/Icons/CriticalIcon.esm.js.map +1 -0
  21. package/dist/components/Icons/EqualsIcon.esm.js +18 -0
  22. package/dist/components/Icons/EqualsIcon.esm.js.map +1 -0
  23. package/dist/components/Icons/IconWithValue.esm.js +16 -0
  24. package/dist/components/Icons/IconWithValue.esm.js.map +1 -0
  25. package/dist/components/Icons/LogsIcon.esm.js +33 -0
  26. package/dist/components/Icons/LogsIcon.esm.js.map +1 -0
  27. package/dist/components/Icons/OutputIcon.esm.js +30 -0
  28. package/dist/components/Icons/OutputIcon.esm.js.map +1 -0
  29. package/dist/components/Icons/SBOMLink.esm.js +32 -0
  30. package/dist/components/Icons/SBOMLink.esm.js.map +1 -0
  31. package/dist/components/PermissionAlert/PermissionAlert.esm.js +9 -0
  32. package/dist/components/PermissionAlert/PermissionAlert.esm.js.map +1 -0
  33. package/dist/components/PipelineRunList/PipelineRunList.esm.js +72 -0
  34. package/dist/components/PipelineRunList/PipelineRunList.esm.js.map +1 -0
  35. package/dist/components/PipelineRunList/PipelineRunOutput.esm.js +23 -0
  36. package/dist/components/PipelineRunList/PipelineRunOutput.esm.js.map +1 -0
  37. package/dist/components/PipelineRunList/PipelineRunSBOMLink.esm.js +51 -0
  38. package/dist/components/PipelineRunList/PipelineRunSBOMLink.esm.js.map +1 -0
  39. package/dist/components/PipelineRunList/PipelineRunTable.esm.js +32 -0
  40. package/dist/components/PipelineRunList/PipelineRunTable.esm.js.map +1 -0
  41. package/dist/components/PipelineRunList/PipelineRunTableBody.esm.js +12 -0
  42. package/dist/components/PipelineRunList/PipelineRunTableBody.esm.js.map +1 -0
  43. package/dist/components/PipelineRunList/PipelineRunTableColumns.esm.js +50 -0
  44. package/dist/components/PipelineRunList/PipelineRunTableColumns.esm.js.map +1 -0
  45. package/dist/components/PipelineRunList/PipelineRunTablePagination.esm.js +130 -0
  46. package/dist/components/PipelineRunList/PipelineRunTablePagination.esm.js.map +1 -0
  47. package/dist/components/PipelineRunList/PipelineRunTableRow.esm.js +81 -0
  48. package/dist/components/PipelineRunList/PipelineRunTableRow.esm.js.map +1 -0
  49. package/dist/components/PipelineRunList/PipelineRunTableRowActions.esm.js +87 -0
  50. package/dist/components/PipelineRunList/PipelineRunTableRowActions.esm.js.map +1 -0
  51. package/dist/components/PipelineRunList/PipelineRunToolbar.esm.js +76 -0
  52. package/dist/components/PipelineRunList/PipelineRunToolbar.esm.js.map +1 -0
  53. package/dist/components/PipelineRunLogs/PipelineRunLogs.esm.js +34 -0
  54. package/dist/components/PipelineRunLogs/PipelineRunLogs.esm.js.map +1 -0
  55. package/dist/components/PipelineRunLogs/PipelineRunLogsDownloader.esm.js +46 -0
  56. package/dist/components/PipelineRunLogs/PipelineRunLogsDownloader.esm.js.map +1 -0
  57. package/dist/components/PipelineRunLogs/PipelineRunStepViewer.esm.js +19 -0
  58. package/dist/components/PipelineRunLogs/PipelineRunStepViewer.esm.js.map +1 -0
  59. package/dist/components/PipelineRunLogs/PipelineRunStepper.esm.js +19 -0
  60. package/dist/components/PipelineRunLogs/PipelineRunStepper.esm.js.map +1 -0
  61. package/dist/components/Router.esm.js +72 -0
  62. package/dist/components/Router.esm.js.map +1 -0
  63. package/dist/components/SecurityViewer/SecurityViewerGithubActionsDetail.esm.js +19 -0
  64. package/dist/components/SecurityViewer/SecurityViewerGithubActionsDetail.esm.js.map +1 -0
  65. package/dist/components/SecurityViewer/SecurityViewerJenkinsDetail.esm.js +19 -0
  66. package/dist/components/SecurityViewer/SecurityViewerJenkinsDetail.esm.js.map +1 -0
  67. package/dist/components/SecurityViewer/SecurityViewerMultiCIPipelines.esm.js +52 -0
  68. package/dist/components/SecurityViewer/SecurityViewerMultiCIPipelines.esm.js.map +1 -0
  69. package/dist/components/SecurityViewer/SecurityViewerPipelineDetailList.esm.js +31 -0
  70. package/dist/components/SecurityViewer/SecurityViewerPipelineDetailList.esm.js.map +1 -0
  71. package/dist/components/SecurityViewer/SecurityViewerPipelineSummaryList.esm.js +32 -0
  72. package/dist/components/SecurityViewer/SecurityViewerPipelineSummaryList.esm.js.map +1 -0
  73. package/dist/components/SecurityViewer/SecurityViewerTabbedMultiCISummaryList.esm.js +26 -0
  74. package/dist/components/SecurityViewer/SecurityViewerTabbedMultiCISummaryList.esm.js.map +1 -0
  75. package/dist/hooks/useDarkTheme.esm.js +21 -0
  76. package/dist/hooks/useDarkTheme.esm.js.map +1 -0
  77. package/dist/hooks/useMssvViewPermisson.esm.js +12 -0
  78. package/dist/hooks/useMssvViewPermisson.esm.js.map +1 -0
  79. package/dist/hooks/usePipelineDetail.esm.js +39 -0
  80. package/dist/hooks/usePipelineDetail.esm.js.map +1 -0
  81. package/dist/hooks/usePipelineSummary.esm.js +38 -0
  82. package/dist/hooks/usePipelineSummary.esm.js.map +1 -0
  83. package/dist/index.d.ts +15 -0
  84. package/dist/index.esm.js +2 -0
  85. package/dist/index.esm.js.map +1 -0
  86. package/dist/models/pipelineRunResult.esm.js +99 -0
  87. package/dist/models/pipelineRunResult.esm.js.map +1 -0
  88. package/dist/plugin.esm.js +81 -0
  89. package/dist/plugin.esm.js.map +1 -0
  90. package/dist/routes.esm.js +18 -0
  91. package/dist/routes.esm.js.map +1 -0
  92. package/dist/types/pipelinerun.esm.js +42 -0
  93. package/dist/types/pipelinerun.esm.js.map +1 -0
  94. package/dist/utils/logs.esm.js +35 -0
  95. package/dist/utils/logs.esm.js.map +1 -0
  96. package/package.json +95 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @backstage-community/plugin-multi-source-security-viewer
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - fda8237: Add multi-source security viewer plugin
8
+
9
+ - displays security information for github actions, jenkins, and gitlab.
10
+
11
+ - Updated dependencies [fda8237]
12
+ - @backstage-community/plugin-multi-source-security-viewer-common@0.1.1
package/README.md ADDED
@@ -0,0 +1,253 @@
1
+ # Multi Source Security Viewer
2
+
3
+ This plugin enables you to visualize pipeline security information from multiple sources.
4
+
5
+ ## Setting up the plugin
6
+
7
+ ### For adminstrators
8
+
9
+ #### Prerequisites
10
+
11
+ 1. Install the CI providers you want the plugin to handle the security scan outputs from.
12
+ The supported CI's are:
13
+
14
+ - [Jenkins](../../../jenkins/plugins/jenkins-backend/README.md)
15
+ - [Github Actions](../../../github-actions/plugins/github-actions/README.md)
16
+ - [Gitlab CI](https://github.com/immobiliare/backstage-plugin-gitlab?tab=readme-ov-file#setup)
17
+
18
+ #### Procedure
19
+
20
+ 1. Install the plugin using the following command:
21
+
22
+ ```
23
+ yarn workspace app add @backstage-community/multi-source-security-viewer
24
+ ```
25
+
26
+ To enable the PipelineRun list in the Security tab on the entity view page, add the following snippet in the packages/app/src/components/catalog/EntityPage.tsx.
27
+
28
+ ```diff
29
+ +import {
30
+ + isMultiCIAvailable,
31
+ + EntityMultiCIPipelinesContent,
32
+ +} from '@backstage-community/plugin-multi-source-security-viewer';
33
+ +
34
+ +import { EntityJenkinsContent } from '@backstage-community/plugin-jenkins';
35
+ +import { EntityGithubActionsContent } from '@backstage-community/plugin-github-actions';
36
+ +
37
+ +const securityContent = (
38
+ + <EntitySwitch>
39
+ + <EntitySwitch.Case if={isMultiCIAvailable}>
40
+ + <EntityMultiCIPipelinesContent />
41
+ + </EntitySwitch.Case>
42
+ + </EntitySwitch>
43
+ +);
44
+ +
45
+ +const entityServicePage = (
46
+ + <EntityLayout.Route path="/security" title="Security">
47
+ + {securityContent}
48
+ + </EntityLayout.Route>
49
+ +);
50
+ ```
51
+
52
+ ## For users
53
+
54
+ ### Using the plugin in Backstage
55
+
56
+ #### Prerequisites
57
+
58
+ 1. Your backstage application is installed and running.
59
+ 2. You have installed the Multi Source Security Viewer plugin.
60
+ 3. Ensure your CI annotations are set:
61
+
62
+ - [Jenkins](../../../jenkins/plugins/jenkins/README.md)
63
+ - [Github Actions]([../../../github-actions/plugins/github-actions/README.md)
64
+ - [Gitlab CI](https://github.com/immobiliare/backstage-plugin-gitlab?tab=readme-ov-file#annotations)
65
+
66
+ ### Procedure
67
+
68
+ 1. Open your Backstage application and select a component from the **Catalog** page.
69
+ 2. Go to the **Security** tab.
70
+
71
+ The **Security** tab displays the list of Pipeline Runs from your CI's based on the component annotations.
72
+
73
+ ![ci-cd-tab-mssv](./docs/images/mssv-multi-ci.png)
74
+
75
+ The table provides a visual representation of identified vulnerabilities in the OCI image produced by the pipelinerun.
76
+ The Author of the pipeline scanner task would provide the CVE summary data using the below format that the UI can interpret.
77
+
78
+ ### Actions buttons
79
+
80
+ #### SBOM
81
+
82
+ Link to SBOM action will be enabled if there is a SBOM task in the pipeline or a external link if provided by the [eye catcher](#format).
83
+ The sbom is captured from the `show-sbom-rhdh` [step](https://github.com/redhat-appstudio/tssc-dev-multi-ci/blob/main/rhtap/show-sbom-rhdh.sh).
84
+
85
+ #### Logs
86
+
87
+ Pipeline steps and their logs are visible if steps have been identified.
88
+
89
+ ![ci-cd-tab-mssv-logs](./docs/images/logs-dialog.png)
90
+
91
+ #### Output
92
+
93
+ Output action will be enabled when the pipeline emits some results and/or contains its [eye catchers](#format).
94
+
95
+ This action opens a modal where it will render the reports for Enterprise contract and Advanced cluster security. The report data should be exposed via logs through EYE_CATCHERS.
96
+
97
+ Supported EYE_CATCHERS:
98
+
99
+ - ACS_IMAGE_SCAN_EYECATCHER_BEGIN
100
+ - ACS_IMAGE_SCAN_EYECATCHER_END
101
+ - ACS_IMAGE_CHECK_EYECATCHER_BEGIN
102
+ - ACS_IMAGE_CHECK_EYECATCHER_END
103
+ - ACS_DEPLOY_EYECATCHER_BEGIN
104
+ - ACS_DEPLOY_EYECATCHER_END
105
+ - EC_EYECATCHER_BEGIN
106
+ - EC_EYECATCHER_END
107
+
108
+ ![ci-cd-tab-mssv-output](./docs/images/mssv-output.png)
109
+
110
+ ### Format:
111
+
112
+ The data is extracted from logs. The steps are identified with `Step : <ANY>`.
113
+
114
+ eg: `Step: cosign-sign-attest`.
115
+
116
+ The logs button will be disabled if no steps are found.
117
+
118
+ Scan results are extracted between EYE_CATCHERS as json.
119
+
120
+ eg: from `ACS_IMAGE_SCAN_EYECATCHER_BEGIN` to `ACS_IMAGE_SCAN_EYECATCHER_END`
121
+
122
+ Log format example:
123
+
124
+ ```
125
+ Step: image-scan
126
+ some-logs
127
+ ...
128
+ ...
129
+ ...
130
+ ACS_IMAGE_SCAN_EYECATCHER_BEGIN
131
+ {
132
+ results: [
133
+ {
134
+ summary: {
135
+ CRITICAL: 0,
136
+ HIGH: 1,
137
+ LOW: 1,
138
+ MEDIUM: 0,
139
+ TOTAL: 2,
140
+ }
141
+ }
142
+ ]
143
+ }
144
+ ACS_IMAGE_SCAN_EYECATCHER_END
145
+ ...
146
+ ...
147
+ some-logs
148
+ ```
149
+
150
+ The above will populate the pipeline run table with its vulnerabilities summary.
151
+
152
+ ### Enterprise contract Task [Optional]:
153
+
154
+ Create a CI step that captures the JSON output between the EC_EYECATCHER:
155
+
156
+ See [example](https://github.com/redhat-appstudio/tssc-dev-multi-ci/blob/main/rhtap/verify-enterprise-contract.sh) script.
157
+
158
+ [Example output](./src/__fixtures__/ec.ts)
159
+
160
+ ### ACS Image scan Task [Optional]:
161
+
162
+ Create a CI step that captures the JSON output between the ACS_IMAGE_SCAN_EYECATCHER:
163
+
164
+ See [example](https://github.com/redhat-appstudio/tssc-dev-multi-ci/blob/main/rhtap/acs-image-scan.sh) script.
165
+
166
+ [Example output](./src/__fixtures__/acsimagescanresult.ts)
167
+
168
+ ### ACS Image check Task [Optional]:
169
+
170
+ Create a CI step that captures the JSON output between the ACS_IMAGE_CHECK_EYECATCHER:
171
+
172
+ See [example](https://github.com/redhat-appstudio/tssc-dev-multi-ci/blob/main/rhtap/acs-image-check.sh) script.
173
+
174
+ [Example output](./src/__fixtures__/acsimagecheckresults.ts)
175
+
176
+ ### ACS Deployment check Task [Optional]:
177
+
178
+ Create a CI step that captures the JSON output between the ACS_DEPLOY_EYECATCHER:
179
+
180
+ See [example](https://github.com/redhat-appstudio/tssc-dev-multi-ci/blob/main/rhtap/acs-deploy-check.sh) script.
181
+
182
+ [Example output](./src/__fixtures__/acsdeploymentcheckresults.ts)
183
+
184
+ ## Additional configuration
185
+
186
+ ### For adminstrators
187
+
188
+ #### Permission Framework Support
189
+
190
+ The Multi Source Security Viewer (MSSV) plugin has support for the permission framework.
191
+
192
+ 1. When [RBAC permission](https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac-backend#installation) framework is enabled, for non-admin users to access MSSV UI, the role associated with your user should have the following permission policies associated with it.
193
+ Add the following in your permission policies configuration file named `rbac-policy.csv`:
194
+
195
+ ```CSV
196
+ p, role:default/team_a, mssv.view.read, read, allow
197
+ ```
198
+
199
+ You can specify the path to this configuration file in your application configuration:
200
+
201
+ ```yaml
202
+ permission:
203
+ enabled: true
204
+ rbac:
205
+ policies-csv-file: /some/path/rbac-policy.csv
206
+ policyFileReload: true
207
+ ```
208
+
209
+ 2. When using the [permission policy](https://backstage.io/docs/permissions/writing-a-policy/) framework.
210
+
211
+ Configure the backend by [configuring the permission policy](https://github.com/backstage/backstage/blob/master/docs/permissions/getting-started.md#test-permission-policy). You can use below policy as example.
212
+
213
+ ```typescript
214
+ import { createBackendModule } from '@backstage/backend-plugin-api';
215
+ import {
216
+ PolicyDecision,
217
+ AuthorizeResult,
218
+ } from '@backstage/plugin-permission-common';
219
+ import {
220
+ PermissionPolicy,
221
+ PolicyQuery,
222
+ PolicyQueryUser,
223
+ } from '@backstage/plugin-permission-node';
224
+ import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha';
225
+
226
+ class MssvPermissionPolicy implements PermissionPolicy {
227
+ async handle(
228
+ request: PolicyQuery,
229
+ user: PolicyQueryUser,
230
+ ): Promise<PolicyDecision> {
231
+ const isGuestUser = user.info.userEntityRef === 'user:development/guest';
232
+ if (isGuestUser && request.permission.name === 'mssv.view.read') {
233
+ return { result: AuthorizeResult.DENY };
234
+ }
235
+ return { result: AuthorizeResult.ALLOW };
236
+ }
237
+ }
238
+
239
+ export default createBackendModule({
240
+ pluginId: 'permission',
241
+ moduleId: 'permission-policy',
242
+ register(reg) {
243
+ reg.registerInit({
244
+ deps: { policy: policyExtensionPoint },
245
+ async init({ policy }) {
246
+ policy.setPolicy(new MssvPermissionPolicy());
247
+ },
248
+ });
249
+ },
250
+ });
251
+ ```
252
+
253
+ ![mssv-rbac](./docs/images/mssv-rbac.png)
@@ -0,0 +1,114 @@
1
+ import { GITHUB_ACTIONS_ANNOTATION, BuildStatus } from '@backstage-community/plugin-github-actions';
2
+ import { createApiRef } from '@backstage/core-plugin-api';
3
+ import { PipelineRunResult } from '../models/pipelineRunResult.esm.js';
4
+ import { RunStatus } from '../types/pipelinerun.esm.js';
5
+
6
+ const mssvGithubActionsApiRef = createApiRef({
7
+ id: "plugin.mssv-api-githubactions.service"
8
+ });
9
+ const mapStatus = (status) => {
10
+ const runStatus = BuildStatus[status];
11
+ switch (runStatus) {
12
+ case BuildStatus.success:
13
+ return RunStatus.Succeeded;
14
+ case BuildStatus.failure:
15
+ return RunStatus.Failed;
16
+ case BuildStatus.pending:
17
+ return RunStatus.Pending;
18
+ case BuildStatus.running:
19
+ return RunStatus.Running;
20
+ default:
21
+ return RunStatus.Unknown;
22
+ }
23
+ };
24
+ class MssvGithubActionsClient {
25
+ githubActionsApi;
26
+ constructor(options) {
27
+ this.githubActionsApi = options.githubActionsApi;
28
+ }
29
+ async getPipelineSummary(options) {
30
+ const { entity, page, pageSize } = options;
31
+ const [owner, repo] = (entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? "/").split("/");
32
+ if (!owner || !repo) {
33
+ return Promise.reject("No repo/owner provided");
34
+ }
35
+ const project = await this.githubActionsApi.listWorkflowRuns({
36
+ owner,
37
+ repo,
38
+ page: page + 1,
39
+ // non zero-based
40
+ pageSize
41
+ });
42
+ const projectWithLogs = await Promise.all(
43
+ project.workflow_runs.map(async (run) => {
44
+ const jobsList = await this.githubActionsApi.listJobsForWorkflowRun({
45
+ owner,
46
+ repo,
47
+ id: run.id
48
+ });
49
+ const logs = await Promise.all(
50
+ jobsList.jobs.map(async (job) => {
51
+ return await this.githubActionsApi.downloadJobLogsForWorkflowRun({
52
+ owner,
53
+ repo,
54
+ runId: job.id
55
+ }).catch(() => "");
56
+ })
57
+ ).then((res) => res.join(" "));
58
+ const status = mapStatus(run?.status ?? "UNKNOWN");
59
+ return {
60
+ run,
61
+ logs,
62
+ status
63
+ };
64
+ })
65
+ );
66
+ const results = projectWithLogs.map(
67
+ (pr) => new PipelineRunResult({
68
+ id: pr?.run?.id.toString(),
69
+ displayName: pr?.run?.display_title,
70
+ status: pr?.status,
71
+ logs: pr?.logs
72
+ })
73
+ );
74
+ return { results, totalCount: project?.total_count ?? 0 };
75
+ }
76
+ async getPipelineDetail(options) {
77
+ const { entity, ref } = options;
78
+ const [owner, repo] = (entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? "/").split("/");
79
+ if (!owner || !repo) {
80
+ return Promise.reject("No repo/owner provided");
81
+ }
82
+ const run = await this.githubActionsApi.getWorkflowRun({
83
+ owner,
84
+ repo,
85
+ id: parseInt(ref, 10)
86
+ });
87
+ const jobsList = await this.githubActionsApi.listJobsForWorkflowRun({
88
+ owner,
89
+ repo,
90
+ id: parseInt(ref, 10)
91
+ });
92
+ const logs = await Promise.all(
93
+ jobsList.jobs.map(async (job) => {
94
+ return await this.githubActionsApi.downloadJobLogsForWorkflowRun({
95
+ owner,
96
+ repo,
97
+ runId: job.id
98
+ }).catch(() => "");
99
+ })
100
+ ).then((res) => res.join(" "));
101
+ const results = [
102
+ new PipelineRunResult({
103
+ id: run?.id.toString(),
104
+ displayName: run?.display_title,
105
+ status: mapStatus(run?.status ?? "UNKNOWN"),
106
+ logs
107
+ })
108
+ ];
109
+ return { results, totalCount: results?.length ?? 0 };
110
+ }
111
+ }
112
+
113
+ export { MssvGithubActionsClient, mssvGithubActionsApiRef };
114
+ //# sourceMappingURL=github.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.esm.js","sources":["../../src/api/github.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n GithubActionsApi,\n GITHUB_ACTIONS_ANNOTATION,\n BuildStatus,\n} from '@backstage-community/plugin-github-actions';\nimport { MssvApi, MssvApiResponse } from './mssv';\nimport { Entity } from '@backstage/catalog-model';\nimport { createApiRef } from '@backstage/core-plugin-api';\nimport { PipelineRunResult } from '../models/pipelineRunResult';\nimport { RunStatus } from '../types/pipelinerun';\n\n// Apiref to map with client\nexport const mssvGithubActionsApiRef = createApiRef<MssvApi>({\n id: 'plugin.mssv-api-githubactions.service',\n});\n\nconst mapStatus = (status: string): RunStatus => {\n const runStatus = BuildStatus[status as keyof typeof BuildStatus];\n switch (runStatus) {\n case BuildStatus.success:\n return RunStatus.Succeeded;\n case BuildStatus.failure:\n return RunStatus.Failed;\n case BuildStatus.pending:\n return RunStatus.Pending;\n case BuildStatus.running:\n return RunStatus.Running;\n default:\n return RunStatus.Unknown;\n }\n};\n\nexport class MssvGithubActionsClient implements MssvApi {\n private readonly githubActionsApi: GithubActionsApi;\n\n constructor(options: { githubActionsApi: GithubActionsApi }) {\n this.githubActionsApi = options.githubActionsApi;\n }\n\n async getPipelineSummary(options: {\n entity: Entity;\n page: number;\n pageSize: number;\n }): Promise<MssvApiResponse> {\n const { entity, page, pageSize } = options;\n const [owner, repo] = (\n entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '/'\n ).split('/');\n\n if (!owner || !repo) {\n return Promise.reject('No repo/owner provided');\n }\n\n const project = await this.githubActionsApi.listWorkflowRuns({\n owner,\n repo,\n page: page + 1, // non zero-based\n pageSize,\n });\n\n const projectWithLogs = await Promise.all(\n project.workflow_runs.map(async run => {\n const jobsList = await this.githubActionsApi.listJobsForWorkflowRun({\n owner,\n repo,\n id: run.id,\n });\n\n const logs = await Promise.all(\n jobsList.jobs.map(async job => {\n return await this.githubActionsApi\n .downloadJobLogsForWorkflowRun({\n owner,\n repo,\n runId: job.id,\n })\n .catch(() => ''); // fallback on empty string. It disables logs button and updates tooltip\n }),\n ).then(res => res.join(' ')); // return as one string\n\n const status = mapStatus(run?.status ?? 'UNKNOWN');\n\n return {\n run,\n logs,\n status,\n };\n }),\n );\n\n const results = projectWithLogs.map(\n pr =>\n new PipelineRunResult({\n id: pr?.run?.id.toString(),\n displayName: pr?.run?.display_title,\n status: pr?.status,\n logs: pr?.logs,\n }),\n );\n\n return { results, totalCount: project?.total_count ?? 0 };\n }\n\n async getPipelineDetail(options: {\n entity: Entity;\n ref: string;\n page: number;\n pageSize: number;\n }): Promise<MssvApiResponse> {\n const { entity, ref } = options;\n const [owner, repo] = (\n entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '/'\n ).split('/');\n\n if (!owner || !repo) {\n return Promise.reject('No repo/owner provided');\n }\n\n const run = await this.githubActionsApi.getWorkflowRun({\n owner,\n repo,\n id: parseInt(ref, 10),\n });\n\n const jobsList = await this.githubActionsApi.listJobsForWorkflowRun({\n owner,\n repo,\n id: parseInt(ref, 10),\n });\n\n const logs = await Promise.all(\n jobsList.jobs.map(async job => {\n return await this.githubActionsApi\n .downloadJobLogsForWorkflowRun({\n owner,\n repo,\n runId: job.id,\n })\n .catch(() => ''); // fallback on empty string. It disables logs button and updates tooltip\n }),\n ).then(res => res.join(' '));\n\n // This a detail view where only 1 item is expected\n const results = [\n new PipelineRunResult({\n id: run?.id.toString(),\n displayName: run?.display_title,\n status: mapStatus(run?.status ?? 'UNKNOWN'),\n logs,\n }),\n ];\n\n return { results, totalCount: results?.length ?? 0 };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ import { GitlabCIClient } from '@immobiliarelabs/backstage-plugin-gitlab';
2
+ import { createApiRef } from '@backstage/core-plugin-api';
3
+ import { PipelineRunResult } from '../models/pipelineRunResult.esm.js';
4
+ import { GitlabPipelineStatus, RunStatus } from '../types/pipelinerun.esm.js';
5
+
6
+ const mssvGitlabCIApiRef = createApiRef({
7
+ id: "plugin.mssv-api-gitlabci.service"
8
+ });
9
+ const mapStatus = (status) => {
10
+ const runStatus = GitlabPipelineStatus[status];
11
+ switch (runStatus) {
12
+ case GitlabPipelineStatus.success:
13
+ return RunStatus.Succeeded;
14
+ case GitlabPipelineStatus.failed:
15
+ return RunStatus.Failed;
16
+ case GitlabPipelineStatus.pending:
17
+ return RunStatus.Pending;
18
+ case GitlabPipelineStatus.running:
19
+ return RunStatus.Running;
20
+ default:
21
+ return RunStatus.Unknown;
22
+ }
23
+ };
24
+ const GITLAB_ANNOTATION_PROJECT_ID = "gitlab.com/project-id";
25
+ const GITLAB_ANNOTATION_PROJECT_SLUG = "gitlab.com/project-slug";
26
+ const GITLAB_ANNOTATION_INSTANCE = "gitlab.com/instance";
27
+ class CustomGitlabCiClient extends GitlabCIClient {
28
+ constructor(options) {
29
+ super(options);
30
+ }
31
+ getPipelineJobs(projectId, pipelineId) {
32
+ return this.callApi(
33
+ `projects/${projectId}/pipelines/${pipelineId}/jobs`,
34
+ {}
35
+ );
36
+ }
37
+ getJobLogs(projectId, jobId) {
38
+ return this.callApi(`projects/${projectId}/jobs/${jobId}/trace`, {});
39
+ }
40
+ }
41
+ class MssvGitlabCIClient {
42
+ gitlabCiApi;
43
+ constructor(options) {
44
+ this.gitlabCiApi = options.gitlabCiApi;
45
+ }
46
+ async getPipelineSummary(options) {
47
+ const { entity, page, pageSize } = options;
48
+ this.gitlabCiApi.gitlabInstance = entity.metadata.annotations?.[GITLAB_ANNOTATION_INSTANCE] ?? "gitlab.com";
49
+ const projectId = entity.metadata.annotations?.[GITLAB_ANNOTATION_PROJECT_ID];
50
+ const projectSlug = entity.metadata.annotations?.[GITLAB_ANNOTATION_PROJECT_SLUG];
51
+ const [sliceStart, sliceEnd] = [
52
+ page * pageSize,
53
+ page * pageSize + pageSize
54
+ ];
55
+ const summary = await this.gitlabCiApi.getPipelineSummary(projectId ?? projectSlug) ?? [];
56
+ const summarySlice = summary.slice(sliceStart, sliceEnd);
57
+ const summaryWithLogs = await Promise.all(
58
+ summarySlice.map(async (run) => {
59
+ const pipelineJobs = await this.gitlabCiApi.getPipelineJobs(run.project_id, run.id) ?? [];
60
+ const logs = await Promise.all(
61
+ pipelineJobs.map(async (job) => {
62
+ return await this.gitlabCiApi.getJobLogs(run.project_id, job.id).catch(() => "");
63
+ }) ?? []
64
+ ).then((res) => res.join(" "));
65
+ return { run, logs };
66
+ })
67
+ );
68
+ const results = summaryWithLogs.map(
69
+ (pr) => new PipelineRunResult({
70
+ id: pr?.run?.id.toString(),
71
+ displayName: pr?.run?.id.toString(),
72
+ status: mapStatus(pr?.run?.status ?? "UNKOWN"),
73
+ logs: pr?.logs
74
+ })
75
+ ) || [];
76
+ return { results, totalCount: summary?.length ?? 0 };
77
+ }
78
+ async getPipelineDetail() {
79
+ return { results: [], totalCount: 0 };
80
+ }
81
+ }
82
+
83
+ export { CustomGitlabCiClient, MssvGitlabCIClient, mssvGitlabCIApiRef };
84
+ //# sourceMappingURL=gitlab.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab.esm.js","sources":["../../src/api/gitlab.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { GitlabCIClient } from '@immobiliarelabs/backstage-plugin-gitlab';\nimport { MssvApi, MssvApiResponse } from './mssv';\nimport { Entity } from '@backstage/catalog-model';\nimport {\n DiscoveryApi,\n IdentityApi,\n OAuthApi,\n createApiRef,\n} from '@backstage/core-plugin-api';\nimport { PipelineRunResult } from '../models/pipelineRunResult';\nimport { GitlabPipelineStatus, RunStatus } from '../types/pipelinerun';\n\n// Apiref to map with client\nexport const mssvGitlabCIApiRef = createApiRef<MssvApi>({\n id: 'plugin.mssv-api-gitlabci.service',\n});\n\nconst mapStatus = (status: string): RunStatus => {\n const runStatus =\n GitlabPipelineStatus[status as keyof typeof GitlabPipelineStatus];\n switch (runStatus) {\n case GitlabPipelineStatus.success:\n return RunStatus.Succeeded;\n case GitlabPipelineStatus.failed:\n return RunStatus.Failed;\n case GitlabPipelineStatus.pending:\n return RunStatus.Pending;\n case GitlabPipelineStatus.running:\n return RunStatus.Running;\n default:\n return RunStatus.Unknown;\n }\n};\n\nconst GITLAB_ANNOTATION_PROJECT_ID = 'gitlab.com/project-id';\nconst GITLAB_ANNOTATION_PROJECT_SLUG = 'gitlab.com/project-slug';\nconst GITLAB_ANNOTATION_INSTANCE = 'gitlab.com/instance';\n\ntype APIOptions = {\n discoveryApi: DiscoveryApi;\n identityApi: IdentityApi;\n codeOwnersPath?: string;\n readmePath?: string;\n gitlabAuthApi: OAuthApi;\n useOAuth?: boolean;\n};\n\nexport class CustomGitlabCiClient extends GitlabCIClient {\n constructor(options: APIOptions) {\n // gitlabInstance is omitted as it is set through the entity\n super(options as any);\n }\n\n getPipelineJobs(\n projectId: number | string,\n pipelineId: number,\n ): Promise<any[] | undefined> {\n return this.callApi(\n `projects/${projectId}/pipelines/${pipelineId}/jobs`,\n {},\n );\n }\n\n getJobLogs(\n projectId: number | string,\n jobId: number,\n ): Promise<string | undefined> {\n return this.callApi(`projects/${projectId}/jobs/${jobId}/trace`, {});\n }\n}\n\nexport class MssvGitlabCIClient implements Partial<MssvApi> {\n private readonly gitlabCiApi: CustomGitlabCiClient;\n\n constructor(options: { gitlabCiApi: CustomGitlabCiClient }) {\n this.gitlabCiApi = options.gitlabCiApi;\n }\n\n async getPipelineSummary(options: {\n entity: Entity;\n page: number;\n pageSize: number;\n }): Promise<MssvApiResponse> {\n const { entity, page, pageSize } = options;\n\n this.gitlabCiApi.gitlabInstance =\n entity.metadata.annotations?.[GITLAB_ANNOTATION_INSTANCE] ?? 'gitlab.com';\n\n const projectId =\n entity.metadata.annotations?.[GITLAB_ANNOTATION_PROJECT_ID];\n\n const projectSlug =\n entity.metadata.annotations?.[GITLAB_ANNOTATION_PROJECT_SLUG];\n\n const [sliceStart, sliceEnd] = [\n page * pageSize,\n page * pageSize + pageSize,\n ];\n\n const summary =\n (await this.gitlabCiApi.getPipelineSummary(projectId ?? projectSlug)) ??\n [];\n\n const summarySlice = summary.slice(sliceStart, sliceEnd);\n const summaryWithLogs = await Promise.all(\n summarySlice.map(async run => {\n const pipelineJobs =\n (await this.gitlabCiApi.getPipelineJobs(run.project_id, run.id)) ??\n [];\n\n const logs = await Promise.all(\n pipelineJobs.map(async job => {\n return await this.gitlabCiApi\n .getJobLogs(run.project_id, job.id)\n .catch(() => ''); // fallback on empty string. It disables logs button and updates tooltip\n }) ?? [],\n ).then(res => res.join(' ')); // return as one\n return { run, logs };\n }),\n );\n\n const results =\n summaryWithLogs.map(\n pr =>\n new PipelineRunResult({\n id: pr?.run?.id.toString(),\n displayName: pr?.run?.id.toString(),\n status: mapStatus(pr?.run?.status ?? 'UNKOWN'),\n logs: pr?.logs,\n }),\n ) || [];\n\n return { results, totalCount: summary?.length ?? 0 };\n }\n\n async getPipelineDetail(): Promise<MssvApiResponse> {\n return { results: [], totalCount: 0 };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ import { getCompoundEntityRef } from '@backstage/catalog-model';
2
+ import { createApiRef } from '@backstage/core-plugin-api';
3
+ import { PipelineRunResult } from '../models/pipelineRunResult.esm.js';
4
+ import { JenkinsRunStatus, RunStatus } from '../types/pipelinerun.esm.js';
5
+
6
+ const mssvJenkinsApiRef = createApiRef({
7
+ id: "plugin.mssv-api-jenkins.service"
8
+ });
9
+ const mapStatus = (status) => {
10
+ const runStatus = JenkinsRunStatus[status];
11
+ switch (runStatus) {
12
+ case JenkinsRunStatus.SUCCESS:
13
+ return RunStatus.Succeeded;
14
+ case JenkinsRunStatus.FAILURE:
15
+ return RunStatus.Failed;
16
+ case JenkinsRunStatus.RUNNING:
17
+ return RunStatus.Running;
18
+ case JenkinsRunStatus["IN PROGRESS"]:
19
+ return RunStatus["In Progress"];
20
+ case JenkinsRunStatus.NOT_BUILT:
21
+ return RunStatus.FailedToStart;
22
+ case JenkinsRunStatus.ABORTED:
23
+ return RunStatus.Cancelled;
24
+ case JenkinsRunStatus.PENDING:
25
+ return RunStatus.Pending;
26
+ default:
27
+ return RunStatus.Unknown;
28
+ }
29
+ };
30
+ class MssvJenkinsClient {
31
+ jenkinsApi;
32
+ constructor(options) {
33
+ this.jenkinsApi = options.jenkinsApi;
34
+ }
35
+ async getPipelineSummary(options) {
36
+ const { entity, page, pageSize } = options;
37
+ const projects = await this.jenkinsApi.getProjects({
38
+ entity: getCompoundEntityRef(entity),
39
+ filter: {}
40
+ // TODO: TO CHECK
41
+ });
42
+ const [sliceStart, sliceEnd] = [
43
+ page * pageSize,
44
+ page * pageSize + pageSize
45
+ ];
46
+ const projectsSlice = projects.slice(sliceStart, sliceEnd);
47
+ const projectWithLogs = await Promise.all(
48
+ projectsSlice.map(async (project) => {
49
+ const { consoleText: logs } = await this.jenkinsApi.getBuildConsoleText({
50
+ jobFullName: project.displayName,
51
+ entity: getCompoundEntityRef(entity),
52
+ buildNumber: (project.lastBuild?.number ?? 0).toString()
53
+ }).catch(() => ({ consoleText: "" })).then((res) => res);
54
+ const run = await this.jenkinsApi.getBuild({
55
+ jobFullName: project.displayName,
56
+ entity: getCompoundEntityRef(entity),
57
+ buildNumber: (project.lastBuild?.number ?? 0).toString()
58
+ });
59
+ const status = mapStatus(project?.status);
60
+ return {
61
+ project,
62
+ run,
63
+ logs,
64
+ status
65
+ };
66
+ })
67
+ );
68
+ const results = projectWithLogs.map(
69
+ (pr) => new PipelineRunResult({
70
+ id: pr?.project?.displayName,
71
+ displayName: pr?.project?.displayName,
72
+ logs: pr?.logs,
73
+ status: pr?.status
74
+ })
75
+ );
76
+ return { results, totalCount: projects?.length ?? 0 };
77
+ }
78
+ async getPipelineDetail(options) {
79
+ const { ref, entity, page, pageSize } = options;
80
+ const job = await this.jenkinsApi.getJobBuilds({
81
+ entity: getCompoundEntityRef(entity),
82
+ jobFullName: ref
83
+ });
84
+ const [sliceStart, sliceEnd] = [
85
+ page * pageSize,
86
+ page * pageSize + pageSize
87
+ ];
88
+ const builds = job?.builds ?? [];
89
+ const buildsSlice = builds.slice(sliceStart, sliceEnd);
90
+ const buildWithLogs = await Promise.all(
91
+ buildsSlice.map(async (run) => {
92
+ const { consoleText: logs } = await this.jenkinsApi.getBuildConsoleText({
93
+ buildNumber: run.number.toString(),
94
+ jobFullName: ref,
95
+ entity: getCompoundEntityRef(entity)
96
+ }).then((res) => res);
97
+ return {
98
+ run,
99
+ logs
100
+ };
101
+ })
102
+ );
103
+ const results = buildsSlice.map((run) => {
104
+ const buildLogEntry = buildWithLogs.find((b) => b?.run?.id === run?.id);
105
+ const logs = buildLogEntry ? buildLogEntry?.logs : "";
106
+ const status = mapStatus(run?.result ?? "UNKNOWN");
107
+ return new PipelineRunResult({
108
+ id: run?.id?.toString(),
109
+ displayName: run?.id?.toString(),
110
+ logs,
111
+ status
112
+ });
113
+ }) || [];
114
+ return { results, totalCount: builds?.length ?? 0 };
115
+ }
116
+ }
117
+
118
+ export { MssvJenkinsClient, mssvJenkinsApiRef };
119
+ //# sourceMappingURL=jenkins.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jenkins.esm.js","sources":["../../src/api/jenkins.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { JenkinsApi } from '@backstage-community/plugin-jenkins';\nimport { Entity, getCompoundEntityRef } from '@backstage/catalog-model';\nimport { createApiRef } from '@backstage/core-plugin-api';\nimport { MssvApi, MssvApiResponse } from './mssv';\nimport { PipelineRunResult } from '../models/pipelineRunResult';\nimport { JenkinsRunStatus, RunStatus } from '../types/pipelinerun';\n\n// Apiref to map with client\nexport const mssvJenkinsApiRef = createApiRef<MssvApi>({\n id: 'plugin.mssv-api-jenkins.service',\n});\n\nconst mapStatus = (status: string): RunStatus => {\n const runStatus = JenkinsRunStatus[status as keyof typeof JenkinsRunStatus];\n switch (runStatus) {\n case JenkinsRunStatus.SUCCESS:\n return RunStatus.Succeeded;\n case JenkinsRunStatus.FAILURE:\n return RunStatus.Failed;\n case JenkinsRunStatus.RUNNING:\n return RunStatus.Running;\n case JenkinsRunStatus['IN PROGRESS']:\n return RunStatus['In Progress'];\n case JenkinsRunStatus.NOT_BUILT:\n return RunStatus.FailedToStart;\n case JenkinsRunStatus.ABORTED:\n return RunStatus.Cancelled;\n case JenkinsRunStatus.PENDING:\n return RunStatus.Pending;\n default:\n return RunStatus.Unknown;\n }\n};\n\nexport class MssvJenkinsClient implements MssvApi {\n private readonly jenkinsApi: JenkinsApi;\n\n constructor(options: { jenkinsApi: JenkinsApi }) {\n this.jenkinsApi = options.jenkinsApi;\n }\n\n async getPipelineSummary(options: {\n entity: Entity;\n page: number;\n pageSize: number;\n }): Promise<MssvApiResponse> {\n const { entity, page, pageSize } = options;\n\n const projects = await this.jenkinsApi.getProjects({\n entity: getCompoundEntityRef(entity),\n filter: {}, // TODO: TO CHECK\n });\n\n const [sliceStart, sliceEnd] = [\n page * pageSize,\n page * pageSize + pageSize,\n ];\n\n const projectsSlice = projects.slice(sliceStart, sliceEnd);\n\n // Fetch logs for each projects last build\n const projectWithLogs = await Promise.all(\n projectsSlice.map(async project => {\n const { consoleText: logs } = await this.jenkinsApi\n .getBuildConsoleText({\n jobFullName: project.displayName,\n entity: getCompoundEntityRef(entity),\n buildNumber: (project.lastBuild?.number ?? 0).toString(),\n })\n .catch(() => ({ consoleText: '' })) // fallback on empty string. It disables logs button and updates tooltip\n .then(res => res);\n\n const run = await this.jenkinsApi.getBuild({\n jobFullName: project.displayName,\n entity: getCompoundEntityRef(entity),\n buildNumber: (project.lastBuild?.number ?? 0).toString(),\n });\n\n const status = mapStatus(project?.status);\n\n return {\n project,\n run,\n logs,\n status,\n };\n }),\n );\n\n const results = projectWithLogs.map(\n pr =>\n new PipelineRunResult({\n id: pr?.project?.displayName,\n displayName: pr?.project?.displayName,\n logs: pr?.logs,\n status: pr?.status,\n }),\n );\n\n return { results, totalCount: projects?.length ?? 0 };\n }\n\n async getPipelineDetail(options: {\n entity: Entity;\n ref: string;\n page: number;\n pageSize: number;\n }): Promise<MssvApiResponse> {\n const { ref, entity, page, pageSize } = options;\n const job = await this.jenkinsApi.getJobBuilds({\n entity: getCompoundEntityRef(entity),\n jobFullName: ref,\n });\n\n // Mimic pagination as Jenkins doesn't provide paginated responses\n const [sliceStart, sliceEnd] = [\n page * pageSize,\n page * pageSize + pageSize,\n ];\n\n const builds = job?.builds ?? [];\n const buildsSlice = builds.slice(sliceStart, sliceEnd);\n // Fetch logs for each project last build\n const buildWithLogs = await Promise.all(\n buildsSlice.map(async run => {\n const { consoleText: logs } = await this.jenkinsApi\n .getBuildConsoleText({\n buildNumber: run.number.toString(),\n jobFullName: ref,\n entity: getCompoundEntityRef(entity),\n })\n .then(res => res);\n\n return {\n run,\n logs,\n };\n }),\n );\n\n // Map builds to PipelineRun\n const results =\n buildsSlice.map(run => {\n const buildLogEntry = buildWithLogs.find(b => b?.run?.id === run?.id);\n const logs = buildLogEntry ? buildLogEntry?.logs : '';\n const status = mapStatus(run?.result ?? 'UNKNOWN');\n return new PipelineRunResult({\n id: run?.id?.toString(),\n displayName: run?.id?.toString(),\n logs,\n status,\n });\n }) || [];\n\n return { results, totalCount: builds?.length ?? 0 };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { ErrorBoundary } from '@backstage/core-components';
2
+ import { Dialog, DialogTitle, Box, IconButton, DialogContent } from '@material-ui/core';
3
+ import CloseIcon from '@material-ui/icons/Close';
4
+ import React from 'react';
5
+
6
+ const DialogLauncher = ({
7
+ open,
8
+ onClose,
9
+ component: Component,
10
+ componentProps = {},
11
+ title,
12
+ ...rest
13
+ }) => {
14
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Dialog, { open, onClose, ...rest }, /* @__PURE__ */ React.createElement(DialogTitle, null, /* @__PURE__ */ React.createElement(
15
+ Box,
16
+ {
17
+ display: "flex",
18
+ justifyContent: { xs: "center", sm: "space-between" },
19
+ alignItems: "center"
20
+ },
21
+ title,
22
+ /* @__PURE__ */ React.createElement(IconButton, { onClick: onClose }, /* @__PURE__ */ React.createElement(CloseIcon, null))
23
+ )), /* @__PURE__ */ React.createElement(DialogContent, null, /* @__PURE__ */ React.createElement(ErrorBoundary, null, /* @__PURE__ */ React.createElement(Component, { ...componentProps })))));
24
+ };
25
+
26
+ export { DialogLauncher };
27
+ //# sourceMappingURL=DialogLauncher.esm.js.map