├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── README.md
├── azure
├── api
│ ├── icon-dark.png
│ └── icon-light.png
├── board.png
├── code
│ ├── icon-dark.png
│ └── icon-light.png
├── license.md
├── projects
│ ├── doc
│ │ ├── 01.png
│ │ ├── 02.png
│ │ ├── 03.png
│ │ ├── 04.png
│ │ ├── 05.png
│ │ └── 06.png
│ ├── icon-dark.png
│ └── icon-light.png
└── radar
│ ├── icon-dark.png
│ └── icon-light.png
├── build.ps1
├── configs
├── dev.json
└── release.json
├── front
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── app.tsx
│ ├── components
│ │ ├── api-docs
│ │ │ ├── api-modal.scss
│ │ │ ├── api-modal.tsx
│ │ │ ├── api-panel.scss
│ │ │ └── api-panel.tsx
│ │ ├── code-quality
│ │ │ ├── code-panel.scss
│ │ │ └── code-panel.tsx
│ │ ├── project
│ │ │ ├── project-modal.scss
│ │ │ ├── project-modal.tsx
│ │ │ ├── project-panel.scss
│ │ │ └── project-panel.tsx
│ │ └── template
│ │ │ ├── template-panel.scss
│ │ │ └── template-panel.tsx
│ ├── index.tsx
│ ├── model
│ │ ├── api.ts
│ │ ├── buildOptions.ts
│ │ ├── code.ts
│ │ ├── project.ts
│ │ ├── sonar.ts
│ │ ├── stacks.ts
│ │ ├── template.ts
│ │ └── user.ts
│ ├── pages
│ │ ├── api-docs
│ │ │ ├── api-page-settings.tsx
│ │ │ ├── api-page.scss
│ │ │ └── api-page.tsx
│ │ ├── code-quality
│ │ │ ├── code-page-settings.tsx
│ │ │ ├── code-page.scss
│ │ │ └── code-page.tsx
│ │ ├── project
│ │ │ ├── projects-page-settings.tsx
│ │ │ ├── projects-page.scss
│ │ │ └── projects-page.tsx
│ │ └── radar
│ │ │ ├── radar.scss
│ │ │ └── radar.tsx
│ ├── react-app-env.d.ts
│ ├── services
│ │ ├── api.ts
│ │ ├── code.ts
│ │ ├── pipeline.ts
│ │ ├── project.ts
│ │ ├── registration.ts
│ │ ├── repository.ts
│ │ ├── services.ts
│ │ ├── sonar.ts
│ │ ├── storage.ts
│ │ └── template.ts
│ └── utils
│ │ └── extensions.ts
└── tsconfig.json
├── package-lock.json
├── tasks
├── stack-board-replaces
│ ├── icon.png
│ ├── package-lock.json
│ ├── package.json
│ ├── stackboardreplaces.ts
│ ├── task.json
│ └── tsconfig.json
└── stack-board-repos
│ ├── package-lock.json
│ ├── package.json
│ ├── stackboardrepos.ts
│ ├── task.json
│ └── tsconfig.json
└── vss-extension.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | */**/node_modules
5 | */**/.pnp
6 | .pnp.js
7 |
8 | # testing
9 | */**/coverage
10 |
11 | # production
12 | */**/build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | *.vsix
25 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "msjsdiag.debugger-for-chrome"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | * Install Chrome Debugger Extension for Visual Studio Code to debug your components with the
4 | * Chrome browser: https://aka.ms/spfx-debugger-extensions
5 | */
6 | "version": "0.2.0",
7 | "configurations": [{
8 | "name": "Local workbench",
9 | "type": "chrome",
10 | "request": "launch",
11 | "url": "https://localhost:4321/temp/workbench.html",
12 | "webRoot": "${workspaceRoot}",
13 | "sourceMaps": true,
14 | "sourceMapPathOverrides": {
15 | "webpack:///.././src/*": "${webRoot}/src/*",
16 | "webpack:///../../../src/*": "${webRoot}/src/*",
17 | "webpack:///../../../../src/*": "${webRoot}/src/*",
18 | "webpack:///../../../../../src/*": "${webRoot}/src/*"
19 | },
20 | "runtimeArgs": [
21 | "--remote-debugging-port=9222"
22 | ]
23 | },
24 | {
25 | "name": "Hosted workbench",
26 | "type": "chrome",
27 | "request": "launch",
28 | "url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx",
29 | "webRoot": "${workspaceRoot}",
30 | "sourceMaps": true,
31 | "sourceMapPathOverrides": {
32 | "webpack:///.././src/*": "${webRoot}/src/*",
33 | "webpack:///../../../src/*": "${webRoot}/src/*",
34 | "webpack:///../../../../src/*": "${webRoot}/src/*",
35 | "webpack:///../../../../../src/*": "${webRoot}/src/*"
36 | },
37 | "runtimeArgs": [
38 | "--remote-debugging-port=9222",
39 | "-incognito"
40 | ]
41 | }
42 | ]
43 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | // Configure glob patterns for excluding files and folders in the file explorer.
4 | "files.exclude": {
5 | "**/.git": true,
6 | "**/.DS_Store": true,
7 | "**/bower_components": true,
8 | "**/node_modules": true,
9 | "**/build": true,
10 |
11 | "**/coverage": true,
12 | "**/lib-amd": true,
13 | "src/**/*.scss.ts": true,
14 | "**/*.vsix": true,
15 | },
16 | "typescript.tsdk": ".\\node_modules\\typescript\\lib"
17 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## **Stack Board for Azure DevOps**
2 |
3 | Designed to speed up the start of new projects by development teams, Stack Board has a template-based process for creating repositories in Azure DevOps.
4 |
5 | It is possible to define several source code models, thus delivering architecture standards and software best practices.
6 |
7 | 
8 |
9 | ## **Features**
10 |
11 | - Creating repositories based on templates.
12 | - Creating Build and Release pipelines (Coming soon).
13 | - Technology Radar (Coming soon).
14 |
15 | ### **Template Catalog**
16 |
17 | 
18 |
19 | | Name | Description |
20 | | ----------------------- | --------------------------------------------------------------------------------------------------- |
21 | | Template | Name of template. e.g. **ASP.NET Core Api Service** |
22 | | Template Description | Description of the template. e.g. **Microservice Template for ASP.NET Core** |
23 | | Source Repository* | Repository with source code that will serve as the basis for projects ASP.NET Core |
24 | | Branch | Name of the branch the source code will be saved to. e.g. **develop**, **main** |
25 | | Replace Key | The keyword that will be changed when creating the project, such as solution name, namespaces, etc. |
26 | | Tags | Tags of technology, such as C#, API, SQLServer |
27 | | Requires authentication | If the source of the base repository is private, it must have its credentials for the git clone |
28 |
29 | * *Even for repositories within Azure DevOps itself, the **"Requires authentication"** field is mandatory to perform the process.
30 |
31 | ### **Create new project**
32 |
33 | 
34 |
35 | | Name | Description |
36 | | --------------- | --------------------------------------------------------------------- |
37 | | Template | Template previously created in the settings screen |
38 | | Name | Project name and word that will be used to replace files and contents |
39 | | Repository Name | Name of the repository that will be created in Azure DevOps |
40 |
41 | ### **Running**
42 |
43 | 
44 |
45 | ### **Successed**
46 |
47 | 
48 |
49 | ### **Requirements**
50 |
51 | - For the process of creating a project, we use Azure Pipelines to perform tasks with git commands, so the permission of the user **"Build Service"** in each **Team Project** must have the following options to **Allow**.
52 |
53 | - Contribute
54 | - Create branch
55 | - Force push
56 |
57 | 
58 |
59 |
60 | ## **License**
61 |
62 | Licensed under the MIT license. More information can be found by viewing the license [here](azure/license.md).
--------------------------------------------------------------------------------
/azure/api/icon-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/api/icon-dark.png
--------------------------------------------------------------------------------
/azure/api/icon-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/api/icon-light.png
--------------------------------------------------------------------------------
/azure/board.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/board.png
--------------------------------------------------------------------------------
/azure/code/icon-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/code/icon-dark.png
--------------------------------------------------------------------------------
/azure/code/icon-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/code/icon-light.png
--------------------------------------------------------------------------------
/azure/license.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Luiz Lima
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/azure/projects/doc/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/projects/doc/01.png
--------------------------------------------------------------------------------
/azure/projects/doc/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/projects/doc/02.png
--------------------------------------------------------------------------------
/azure/projects/doc/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/projects/doc/03.png
--------------------------------------------------------------------------------
/azure/projects/doc/04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/projects/doc/04.png
--------------------------------------------------------------------------------
/azure/projects/doc/05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/projects/doc/05.png
--------------------------------------------------------------------------------
/azure/projects/doc/06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/projects/doc/06.png
--------------------------------------------------------------------------------
/azure/projects/icon-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/projects/icon-dark.png
--------------------------------------------------------------------------------
/azure/projects/icon-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/projects/icon-light.png
--------------------------------------------------------------------------------
/azure/radar/icon-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/radar/icon-dark.png
--------------------------------------------------------------------------------
/azure/radar/icon-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/azure/radar/icon-light.png
--------------------------------------------------------------------------------
/build.ps1:
--------------------------------------------------------------------------------
1 | # npm run build --prefix front
2 | # npm run build --prefix tasks/stack-board-repos
3 | # npm run build --prefix tasks/stack-board-replaces
4 |
5 | tfx extension create --manifest-globs vss-extension.json --overrides-file ./configs/release.json --root ./
--------------------------------------------------------------------------------
/configs/dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "stack-board-dev",
3 | "name": "Stack Board DEV",
4 | "public": false,
5 | "baseUri": "https://localhost:3000"
6 | }
7 |
--------------------------------------------------------------------------------
/configs/release.json:
--------------------------------------------------------------------------------
1 | {
2 | "galleryFlags": [
3 | "Public"
4 | ],
5 | "public": true
6 | }
--------------------------------------------------------------------------------
/front/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stack-board",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "Stack board extension for Azure DevOps",
6 | "author": "IT Team",
7 | "license": "ISC",
8 | "homepage": "./",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/llima/stack-board-extension.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/llima/stack-board-extension/issues"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "release": "npm run package-release",
20 | "package-release": "tfx extension create --manifest-globs ../vss-extension.json --overrides-file ../configs/release.json --root ../"
21 | },
22 | "dependencies": {
23 | "@fluentui/example-data": "^8.2.3",
24 | "@fluentui/react": "^8.22.2",
25 | "@types/node": "^12.0.0",
26 | "@types/react": "^17.0.0",
27 | "@types/react-dom": "^17.0.0",
28 | "azure-devops-extension-api": "^1.152.3",
29 | "azure-devops-extension-sdk": "^2.0.10",
30 | "azure-devops-ui": "^2.167.7",
31 | "base-64": "^1.0.0",
32 | "guid-typescript": "^1.0.9",
33 | "react": "^17.0.2",
34 | "react-dom": "^17.0.2",
35 | "react-icons": "^4.2.0",
36 | "react-scripts": "4.0.3",
37 | "swagger-ui-react": "3.25.0",
38 | "typescript": "^4.1.2",
39 | "vss-web-extension-sdk": "^5.141.0"
40 | },
41 | "devDependencies": {
42 | "base64-inline-loader": "^1.1.1",
43 | "node-sass": "^4.13.0",
44 | "tslint-react": "^4.0.0"
45 | },
46 | "eslintConfig": {
47 | "extends": [
48 | "react-app"
49 | ]
50 | },
51 | "browserslist": {
52 | "production": [
53 | ">0.2%",
54 | "not dead",
55 | "not op_mini all"
56 | ],
57 | "development": [
58 | "last 1 chrome version",
59 | "last 1 firefox version",
60 | "last 1 safari version"
61 | ]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/front/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/front/public/favicon.ico
--------------------------------------------------------------------------------
/front/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Stack Board
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/front/src/app.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as DevOps from "azure-devops-extension-sdk";
3 |
4 | import ProjectPage from './pages/project/projects-page';
5 | import Radar from './pages/radar/radar';
6 |
7 | import { IHostNavigationService } from 'azure-devops-extension-api/Common/CommonServices';
8 | import Api from './pages/api-docs/api-page';
9 | import Code from './pages/code-quality/code-page';
10 |
11 | interface IAppState {
12 | page: string;
13 | }
14 |
15 | class App extends React.Component<{}, IAppState> {
16 |
17 | projectService = DevOps.getService(
18 | "ms.vss-features.host-navigation-service"
19 | );
20 |
21 | constructor(props: {}) {
22 | super(props);
23 |
24 | DevOps.init();
25 | this.state = {page: ""};
26 |
27 | this.projectService.then(item => {
28 | item.getPageRoute().then(route => {
29 | this.setState({ page: route.routeValues["parameters"] });
30 | });
31 | });
32 | }
33 |
34 | render() {
35 |
36 | const { page } = this.state;
37 |
38 | switch (page) {
39 | case "llima.stack-board.stack-board-hub":
40 | return ();
41 | case "llima.stack-board.tech-radar-hub":
42 | return ();
43 | case "llima.stack-board.api-docs-hub":
44 | return ();
45 | case "llima.stack-board.code-quality-hub":
46 | return (
);
47 | default:
48 | return null;
49 | }
50 | }
51 | }
52 |
53 | export default App;
54 |
--------------------------------------------------------------------------------
/front/src/components/api-docs/api-modal.scss:
--------------------------------------------------------------------------------
1 | @import "azure-devops-ui/Core/_platformCommon.scss";
2 |
3 | .api-modal {
4 |
5 | &--group {
6 | margin-bottom: 20px;
7 | }
8 | &--content {
9 | padding-top: 20px;
10 | }
11 |
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/front/src/components/api-docs/api-modal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './api-modal.scss';
3 |
4 | import { TextField } from "azure-devops-ui/TextField";
5 | import { IApi } from '../../model/api';
6 |
7 | import { CustomDialog } from "azure-devops-ui/Dialog";
8 | import { PanelContent, PanelFooter } from "azure-devops-ui/Panel";
9 | import { CustomHeader, HeaderTitleArea } from 'azure-devops-ui/Header';
10 | import { ButtonGroup } from 'azure-devops-ui/ButtonGroup';
11 | import { Button } from 'azure-devops-ui/Button';
12 |
13 |
14 | export interface IApiModalProps {
15 | show: boolean;
16 | onDismiss: any;
17 | onConfirm: any;
18 | api: IApi;
19 | }
20 |
21 | interface IApiModalState {
22 | nameValidation: string;
23 | }
24 |
25 | class ApiModal extends React.Component {
26 |
27 | constructor(props: IApiModalProps) {
28 | super(props);
29 | this.state = {
30 | nameValidation: ""
31 | };
32 | }
33 |
34 | isDeleteValid(): boolean {
35 | return (
36 | this.props.api.name.toLocaleLowerCase() === this.state.nameValidation.toLocaleLowerCase()
37 | );
38 | }
39 |
40 | close(that: this) {
41 | that.setState({ nameValidation: "" }, () => {
42 | that.props.onDismiss();
43 | });
44 | }
45 |
46 | confirm(that: this) {
47 | that.setState({ nameValidation: "", }, () => {
48 | that.props.onConfirm();
49 | });
50 | }
51 |
52 | render() {
53 |
54 | const { nameValidation } = this.state;
55 | const { api } = this.props;
56 |
57 | if (this.props.show) {
58 | return (
59 | this.close(this)}
61 | modal={true}>
62 |
63 |
64 |
65 |
66 | Delete '{api.name}'?
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | This api will be permanently deleted. This is a destructive operation.
75 |
76 |
77 | this.setState({ nameValidation: value })}
82 | />
83 |
84 |
85 |
86 |
87 |
88 |
98 |
99 |
100 | );
101 | }
102 | return null;
103 | }
104 | }
105 |
106 | export default ApiModal;
107 |
--------------------------------------------------------------------------------
/front/src/components/api-docs/api-panel.scss:
--------------------------------------------------------------------------------
1 | @import "azure-devops-ui/Core/_platformCommon.scss";
2 |
3 | .api {
4 | &--content {
5 | display: flex;
6 | flex-grow: 1;
7 | flex-direction: column;
8 |
9 | // Reserve space for focus
10 | padding: 4px 0;
11 | }
12 |
13 | &--group {
14 | margin-bottom: 20px;
15 |
16 | &-label {
17 | display: block;
18 | padding: 5px 0;
19 | font-size: $fontSizeML;
20 | font-weight: $fontWeightSemiBold;
21 | }
22 | }
23 |
24 | &--add-button {
25 | text-align: right;
26 | }
27 |
28 | &--list-row
29 | {
30 | padding: 0px 12px;
31 | }
32 |
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/front/src/components/api-docs/api-panel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './api-panel.scss';
3 |
4 | import { Panel } from "azure-devops-ui/Panel";
5 | import { TextField } from "azure-devops-ui/TextField";
6 | import { IApi } from '../../model/api';
7 | import { Guid } from 'guid-typescript';
8 | import { Services } from '../../services/services';
9 | import { IApiService, ApiServiceId } from '../../services/api';
10 |
11 | export interface IApiPanelProps {
12 | show: boolean;
13 | onDismiss: any;
14 | }
15 |
16 | interface IApiPanelState {
17 | currentApi: IApi;
18 | creating: boolean;
19 | }
20 |
21 | class ApiPanel extends React.Component {
22 |
23 | service = Services.getService(
24 | ApiServiceId
25 | );
26 |
27 | constructor(props: IApiPanelProps) {
28 | super(props);
29 | this.state = {
30 | currentApi: this.getStartValue(),
31 | creating: false
32 | };
33 | }
34 |
35 | getStartValue(): IApi {
36 | return {
37 | id: "",
38 | name: "",
39 | url: ""
40 | };
41 | }
42 |
43 | close(that: this) {
44 | that.setState({ currentApi: that.getStartValue(), creating: false }, () => {
45 | that.props.onDismiss();
46 | });
47 | }
48 |
49 | onInputChange(event: React.ChangeEvent, value: string, that: this) {
50 | var prop = event.target.id.replace("__bolt-", "");
51 | that.setState(prevState => ({
52 | currentApi: {...prevState.currentApi, [prop]: value}
53 | }));
54 | }
55 |
56 | isValid(): boolean {
57 | const { currentApi } = this.state;
58 |
59 | return (
60 | currentApi.name && currentApi.name.trim() !== "" &&
61 | currentApi.url && currentApi.url.trim() !== ""
62 | );
63 | }
64 |
65 | async createNewApi(that: this) {
66 |
67 | that.setState({ creating: true });
68 |
69 | var item = that.state.currentApi;
70 | item.id = Guid.create().toString();
71 |
72 | that.service.saveApi(item).then(item => {
73 | that.close(that);
74 | });
75 | }
76 |
77 | render() {
78 |
79 | const { currentApi, creating } = this.state;
80 |
81 | if (this.props.show) {
82 | return (
83 | {
85 | this.close(this);
86 | }}
87 | titleProps={{ text: "Register new api" }}
88 | description={"Register new api from url swagger."}
89 | footerButtonProps={[
90 | {
91 | text: "Cancel", onClick: (event) => {
92 | this.close(this);
93 | }
94 | },
95 | {
96 | text: creating ? "Registring..." : "Register",
97 | primary: true,
98 | onClick: (event) => {
99 | this.createNewApi(this)
100 | },
101 | disabled: !this.isValid() || creating
102 | }
103 | ]}>
104 |
105 |
106 |
107 |
108 | this.onInputChange(event, value, this)}
114 | />
115 |
116 |
117 |
118 |
119 | this.onInputChange(event, value, this)}
125 | />
126 |
127 |
128 |
129 |
130 | );
131 | }
132 | return null;
133 | }
134 | }
135 |
136 | export default ApiPanel;
137 |
--------------------------------------------------------------------------------
/front/src/components/code-quality/code-panel.scss:
--------------------------------------------------------------------------------
1 | @import "azure-devops-ui/Core/_platformCommon.scss";
2 |
3 | .code {
4 | &--content {
5 | display: flex;
6 | flex-grow: 1;
7 | flex-direction: column;
8 |
9 | // Reserve space for focus
10 | padding: 4px 0;
11 | }
12 |
13 | &--group {
14 | margin-bottom: 20px;
15 |
16 | &-label {
17 | display: block;
18 | padding: 5px 0;
19 | font-size: $fontSizeML;
20 | font-weight: $fontWeightSemiBold;
21 | }
22 | }
23 |
24 | &--add-button {
25 | text-align: right;
26 | float: right;
27 | }
28 |
29 | &--list-row
30 | {
31 | padding: 0px 12px;
32 | }
33 |
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/front/src/components/code-quality/code-panel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './code-panel.scss';
3 |
4 | import { Panel } from "azure-devops-ui/Panel";
5 | import { TextField } from "azure-devops-ui/TextField";
6 | import { ICode } from '../../model/code';
7 | import { Guid } from 'guid-typescript';
8 | import { Services } from '../../services/services';
9 | import { ICodeService, CodeServiceId } from '../../services/code';
10 | import { ButtonGroup } from 'azure-devops-ui/ButtonGroup';
11 | import { Button } from 'azure-devops-ui/Button';
12 | import { Card } from 'azure-devops-ui/Card';
13 | import { Checkbox } from "azure-devops-ui/Checkbox";
14 |
15 | import { ISonarService, SonarServiceId } from '../../services/sonar';
16 | import { ISonarComponent } from '../../model/sonar';
17 | import { MessageCard, MessageCardSeverity } from 'azure-devops-ui/MessageCard';
18 |
19 | export interface ICodePanelProps {
20 | show: boolean;
21 | onDismiss: any;
22 | }
23 |
24 | interface ICodePanelState {
25 | currentCode: ICode;
26 | creating: boolean;
27 | hasLoadError: boolean;
28 | components?: ISonarComponent[];
29 | }
30 |
31 | class CodePanel extends React.Component {
32 |
33 | storageService = Services.getService(CodeServiceId);
34 | sonarService = Services.getService(SonarServiceId);
35 |
36 | constructor(props: ICodePanelProps) {
37 | super(props);
38 | this.state = {
39 | currentCode: this.getStartValue(),
40 | creating: false,
41 | hasLoadError: false,
42 | components: []
43 | };
44 | }
45 |
46 | getStartValue(): ICode {
47 | return {
48 | id: "",
49 | type: "Sonarqube",
50 | server: "",
51 | token: "",
52 | components: []
53 | };
54 | }
55 |
56 | onInputChange(event: React.ChangeEvent, value: string, that: this) {
57 | var prop = event.target.id.replace("__bolt-", "");
58 | that.setState(prevState => ({
59 | currentCode: { ...prevState.currentCode, [prop]: value }
60 | }));
61 | }
62 |
63 | isValid(): boolean {
64 | const { currentCode } = this.state;
65 |
66 | return (
67 | currentCode.server && currentCode.server.trim() !== "" &&
68 | currentCode.token && currentCode.token.trim() !== ""
69 | );
70 | }
71 |
72 | addComponent(component: ISonarComponent, checked: boolean, that: this) {
73 |
74 | var items = that.state.currentCode.components;
75 |
76 | if (checked)
77 | items.push(component.key)
78 | else
79 | items = items.filter(d => d !== component.key);
80 |
81 | that.setState(prevState => ({
82 | currentCode: { ...prevState.currentCode, components: items }
83 | }));
84 |
85 | }
86 |
87 | load(that: this) {
88 | var item = that.state.currentCode;
89 |
90 | that.setState(prevState => ({
91 | currentCode: { ...prevState.currentCode, components: [] },
92 | hasLoadError: false
93 | }));
94 |
95 | that.sonarService.loadComponents(item.server, item.token)
96 | .then((items: ISonarComponent[]) => {
97 | that.setState({ components: items })
98 | })
99 | .catch(function (error) {
100 | console.log(error);
101 | that.setState({ hasLoadError: true });
102 | });;
103 | }
104 |
105 | save(that: this) {
106 | that.setState({ creating: true });
107 | var item = that.state.currentCode;
108 | item.id = Guid.create().toString();
109 |
110 | that.storageService.removeAll().then(() => { });
111 | that.storageService.saveCode(item).then(item => {
112 | that.close(that);
113 | });
114 | }
115 |
116 | close(that: this) {
117 | that.setState({ currentCode: that.getStartValue(), creating: false }, () => {
118 | that.props.onDismiss();
119 | });
120 | }
121 |
122 | render() {
123 |
124 | const { currentCode, creating, components, hasLoadError } = this.state;
125 |
126 | if (this.props.show) {
127 | return (
128 | {
130 | this.close(this);
131 | }}
132 | titleProps={{ text: "Register new tool" }}
133 | description={"Register new code quality tool."}
134 | footerButtonProps={[
135 | {
136 | text: "Cancel", onClick: (event) => {
137 | this.close(this);
138 | }
139 | },
140 | {
141 | text: creating ? "Registring..." : "Register",
142 | primary: true,
143 | onClick: (event) => {
144 | this.save(this)
145 | },
146 | disabled: !this.isValid() || creating || this.state.currentCode.components.length === 0
147 | }
148 | ]}>
149 |
150 |
151 |
152 |
153 | this.onInputChange(event, value, this)}
158 | placeholder="e.g. https://sonarqube.company.com"
159 | />
160 |
161 |
162 |
163 | this.onInputChange(event, value, this)}
168 | inputType={"password"}
169 | required={true}
170 | placeholder="01a0164e6f112950das79d823001507cd4fdffce"
171 | />
172 |
173 |
174 |
175 |
176 | this.load(this)} />
181 |
182 |
183 |
184 |
185 |
186 | {hasLoadError &&
189 | Error loading projects from server.
190 | }
191 |
192 | {!hasLoadError && components.length > 0 &&
193 |
194 | {components.map(item => (
195 | (this.addComponent(item, checked, this))}
197 | checked={currentCode.components.filter(d => d === item.key).length > 0}
198 | label={item.key}
199 | />
200 | ))}
201 |
202 | }
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 | );
211 | }
212 | return null;
213 | }
214 | }
215 |
216 | export default CodePanel;
217 |
--------------------------------------------------------------------------------
/front/src/components/project/project-modal.scss:
--------------------------------------------------------------------------------
1 | @import "azure-devops-ui/Core/_platformCommon.scss";
2 |
3 | .project-modal {
4 |
5 | &--group {
6 | margin-bottom: 20px;
7 | }
8 | &--content {
9 | padding-top: 20px;
10 | }
11 |
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/front/src/components/project/project-modal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './project-modal.scss';
3 |
4 | import { TextField } from "azure-devops-ui/TextField";
5 | import { IProject } from '../../model/project';
6 |
7 | import { RadioButton, RadioButtonGroup } from "azure-devops-ui/RadioButton";
8 | import { CustomDialog } from "azure-devops-ui/Dialog";
9 | import { PanelContent, PanelFooter } from "azure-devops-ui/Panel";
10 | import { CustomHeader, HeaderTitleArea } from 'azure-devops-ui/Header';
11 | import { ButtonGroup } from 'azure-devops-ui/ButtonGroup';
12 | import { Button } from 'azure-devops-ui/Button';
13 |
14 |
15 | export interface IProjectModalProps {
16 | show: boolean;
17 | onDismiss: any;
18 | onConfirm: any;
19 | project: IProject;
20 | }
21 |
22 | interface IProjectModalState {
23 | nameValidation: string;
24 | deleteType: string;
25 | }
26 |
27 | class ProjectModal extends React.Component {
28 |
29 | constructor(props: IProjectModalProps) {
30 | super(props);
31 | this.state = {
32 | nameValidation: "",
33 | deleteType: "project"
34 | };
35 | }
36 |
37 | isDeleteValid(): boolean {
38 | return (
39 | this.props.project.name.toLocaleLowerCase() === this.state.nameValidation.toLocaleLowerCase()
40 | );
41 | }
42 |
43 | close(that: this) {
44 | that.setState({ nameValidation: "", deleteType: "project" }, () => {
45 | that.props.onDismiss();
46 | });
47 | }
48 |
49 | confirm(that: this) {
50 | var type = that.state.deleteType;
51 | that.setState({ nameValidation: "", deleteType: "project" }, () => {
52 | that.props.onConfirm(type);
53 | });
54 | }
55 |
56 | render() {
57 |
58 | const { nameValidation, deleteType } = this.state;
59 | const { project } = this.props;
60 |
61 | if (this.props.show) {
62 | return (
63 | this.close(this)}
65 | modal={true}>
66 |
67 |
68 |
69 |
70 | Delete '{project.name}'?
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | This project will be permanently deleted. This is a destructive operation.
79 |
80 |
{ this.setState({ deleteType: selectedId })}}
82 | selectedButtonId={deleteType}
83 | text={"What type of exclusion?"}>
84 |
85 |
86 |
87 |
88 | this.setState({ nameValidation: value })}
93 | />
94 |
95 |
96 |
97 |
98 |
99 | this.close(this)} />
103 | this.confirm(this)} />
108 |
109 |
110 |
111 | );
112 | }
113 | return null;
114 | }
115 | }
116 |
117 | export default ProjectModal;
118 |
--------------------------------------------------------------------------------
/front/src/components/project/project-panel.scss:
--------------------------------------------------------------------------------
1 | @import "azure-devops-ui/Core/_platformCommon.scss";
2 |
3 | .project {
4 | &--content {
5 | display: flex;
6 | flex-grow: 1;
7 | flex-direction: column;
8 |
9 | // Reserve space for focus
10 | padding: 4px 0;
11 | }
12 |
13 | &--group {
14 | margin-bottom: 20px;
15 |
16 | &-label {
17 | display: block;
18 | padding: 5px 0;
19 | font-size: $fontSizeML;
20 | font-weight: $fontWeightSemiBold;
21 | }
22 | }
23 |
24 | &--add-button {
25 | text-align: right;
26 | }
27 |
28 | &--list-row
29 | {
30 | padding: 0px 12px;
31 | }
32 |
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/front/src/components/project/project-panel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './project-panel.scss';
3 |
4 | import { Panel } from "azure-devops-ui/Panel";
5 | import { TextField } from "azure-devops-ui/TextField";
6 | import { Dropdown } from "azure-devops-ui/Dropdown";
7 |
8 | import { CreateRepositoryAsync } from '../../services/repository';
9 | import { CreateBuildDefinitionAsync, RunBuildAsync } from '../../services/pipeline';
10 | import { ITemplate } from '../../model/template';
11 | import { IProject, ProjectStatus } from '../../model/project';
12 | import { Guid } from 'guid-typescript';
13 | import { Services } from '../../services/services';
14 | import { IProjectService, ProjectServiceId } from '../../services/project';
15 | import { MessageCard, MessageCardSeverity } from "azure-devops-ui/MessageCard";
16 | import { FormItem } from "azure-devops-ui/FormItem";
17 | import { ObservableValue } from 'azure-devops-ui/Core/Observable';
18 |
19 | import * as DevOps from "azure-devops-extension-sdk";
20 |
21 | export interface IProjectPanelProps {
22 | show: boolean;
23 | onDismiss: any;
24 | templates: ITemplate[];
25 | projects: IProject[];
26 | }
27 |
28 | interface IProjectPanelState {
29 | currentProject: IProject;
30 | nameIsValid: boolean;
31 | repoIsValid: boolean;
32 | creating: boolean;
33 | }
34 |
35 | class ProjectPanel extends React.Component {
36 |
37 | errorObservable = new ObservableValue("");
38 | service = Services.getService(
39 | ProjectServiceId
40 | );
41 |
42 | constructor(props: IProjectPanelProps) {
43 | super(props);
44 | this.state = {
45 | currentProject: this.getStartValue(),
46 | creating: false,
47 | nameIsValid: true,
48 | repoIsValid: true
49 | };
50 | }
51 |
52 | getStartValue(): IProject {
53 | return {
54 | id: "",
55 | name: "",
56 | team: "",
57 | repoName: "",
58 | status: ProjectStatus.Running,
59 | template: null,
60 | user: null
61 | };
62 | }
63 |
64 | close(that: this) {
65 | that.setState({ currentProject: that.getStartValue(), creating: false }, () => {
66 | that.props.onDismiss();
67 | });
68 | }
69 |
70 | onInputChange(event: React.ChangeEvent, value: string, that: this) {
71 | var prop = event.target.id.replace("__bolt-", "");
72 | that.state.currentProject[prop] = value;
73 |
74 | if (prop === "name" && that.props.projects.filter(d => d.name.toLocaleLowerCase() === that.state.currentProject.name.toLocaleLowerCase()).length > 0) {
75 | that.setState({ nameIsValid: false, currentProject: that.state.currentProject });
76 | return;
77 | }
78 |
79 | if (prop === "repoName" && that.props.projects.filter(d => d.repoName.toLocaleLowerCase() === that.state.currentProject.repoName.toLocaleLowerCase()).length > 0) {
80 | that.setState({ repoIsValid: false, currentProject: that.state.currentProject });
81 | return;
82 | }
83 |
84 | this.setState({ currentProject: that.state.currentProject, nameIsValid: true, repoIsValid: true });
85 | }
86 |
87 | isValid(): boolean {
88 | const { currentProject, repoIsValid, nameIsValid } = this.state;
89 |
90 | return (
91 | repoIsValid && nameIsValid &&
92 | currentProject.name && currentProject.name.trim() !== "" &&
93 | currentProject.template && currentProject.template.id.trim() !== "" &&
94 | currentProject.repoName && currentProject.repoName.trim() !== ""
95 | );
96 | }
97 |
98 | async createNewProject(that: this) {
99 |
100 | that.setState({ creating: true });
101 |
102 | var user = DevOps.getUser();
103 | var item = that.state.currentProject;
104 |
105 | var repository = await CreateRepositoryAsync(item.repoName);
106 | var buildDef = await CreateBuildDefinitionAsync({
107 | name: item.name,
108 | team: item.team,
109 | repositoryId: repository.id,
110 | template: item.template,
111 | user: user
112 | });
113 | var runDuild = await RunBuildAsync(buildDef.id);
114 |
115 | item.id = Guid.create().toString();
116 |
117 | item.user = user;
118 | item.repoUrl = repository.webUrl;
119 | item.repoId = repository.id;
120 | item.runBuildId = runDuild.id;
121 | item.buildDefinitionId = buildDef.id;
122 | item.startTime = new Date();
123 |
124 | that.service.saveProject(item).then(item => {
125 | that.close(that);
126 | });
127 | }
128 |
129 | render() {
130 |
131 | const { currentProject, nameIsValid, repoIsValid, creating } = this.state;
132 |
133 | if (this.props.show) {
134 | return (
135 | {
137 | this.close(this);
138 | }}
139 | titleProps={{ text: "Create new project" }}
140 | description={"Create new project from a template."}
141 | footerButtonProps={[
142 | {
143 | text: "Cancel", onClick: (event) => {
144 | this.close(this);
145 | }
146 | },
147 | {
148 | text: creating ? "Creating..." : "Create",
149 | primary: true,
150 | onClick: (event) => {
151 | this.createNewProject(this)
152 | },
153 | disabled: !this.isValid() || creating
154 | }
155 | ]}>
156 |
157 |
158 |
159 |
160 | {
166 | this.setState(prevState => ({
167 | currentProject: {...prevState.currentProject, template: item as ITemplate}
168 | }));
169 | }}
170 | />
171 |
172 |
173 | {currentProject.template &&
174 |
177 | {currentProject.template.description}
178 |
179 |
}
180 |
181 |
182 |
183 | this.onInputChange(event, value, this)}
189 | />
190 |
191 |
192 |
193 |
194 | this.onInputChange(event, value, this)}
200 | />
201 |
202 |
203 |
204 |
205 | this.onInputChange(event, value, this)}
211 | />
212 |
213 |
214 |
215 |
216 |
217 |
218 | );
219 | }
220 | return null;
221 | }
222 | }
223 |
224 | export default ProjectPanel;
225 |
--------------------------------------------------------------------------------
/front/src/components/template/template-panel.scss:
--------------------------------------------------------------------------------
1 | @import "azure-devops-ui/Core/_platformCommon.scss";
2 |
3 | .template {
4 | &--content {
5 | display: flex;
6 | flex-grow: 1;
7 | flex-direction: column;
8 |
9 | // Reserve space for focus
10 | padding: 4px 0;
11 | }
12 |
13 | &--group {
14 | margin-bottom: 20px;
15 |
16 | &-label {
17 | display: block;
18 | padding: 5px 0;
19 | font-size: $fontSizeML;
20 | font-weight: $fontWeightSemiBold;
21 | }
22 | }
23 |
24 | &--add-button {
25 | text-align: right;
26 | float: right;
27 | }
28 |
29 | &--list-row
30 | {
31 | padding: 0px 12px;
32 | }
33 |
34 | }
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/front/src/components/template/template-panel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './template-panel.scss';
3 |
4 | import {
5 | ScrollableList,
6 | IListItemDetails,
7 | ListSelection,
8 | ListItem,
9 | IListRow
10 | } from "azure-devops-ui/List";
11 |
12 | import { Guid } from "guid-typescript";
13 | import { Panel } from "azure-devops-ui/Panel";
14 | import { TextField } from "azure-devops-ui/TextField";
15 | import { Card } from 'azure-devops-ui/Card';
16 | import { ArrayItemProvider } from "azure-devops-ui/Utilities/Provider";
17 |
18 | import { Toggle } from "azure-devops-ui/Toggle";
19 | import { Button } from "azure-devops-ui/Button";
20 |
21 | import { Services } from "../../services/services";
22 | import { ITemplate } from '../../model/template';
23 | import { ITemplateService, TemplateServiceId } from "../../services/template";
24 | import { Observer } from 'azure-devops-ui/Observer';
25 | import { ObservableValue } from 'azure-devops-ui/Core/Observable';
26 | import { ButtonGroup } from 'azure-devops-ui/ButtonGroup';
27 | import { IStack, StackValues } from '../../model/stacks';
28 | import { TagPicker } from "azure-devops-ui/TagPicker";
29 | import { ISuggestionItemProps } from "azure-devops-ui/SuggestionsList";
30 | import { PillGroup } from 'azure-devops-ui/PillGroup';
31 | import { Pill, PillSize } from 'azure-devops-ui/Pill';
32 |
33 |
34 | export interface ITemplatePanelProps {
35 | show: boolean;
36 | onDismiss: any;
37 | }
38 |
39 | interface ITemplatePanelState {
40 | showAuthentication: boolean;
41 | currentTemplate?: ITemplate;
42 | templates: ITemplate[];
43 | tagSuggestions: IStack[]
44 | }
45 |
46 | class TemplatePanel extends React.Component {
47 |
48 | selection = new ListSelection(true);
49 |
50 | service = Services.getService(
51 | TemplateServiceId
52 | );
53 |
54 | constructor(props: ITemplatePanelProps) {
55 | super(props);
56 |
57 | this.state = {
58 | showAuthentication: false,
59 | currentTemplate: this.getStartValue(),
60 | tagSuggestions: StackValues,
61 | templates: [],
62 | };
63 |
64 | this.service.getTemplate().then(items => {
65 | this.setState({
66 | templates: items.sortByProp("text")
67 | });
68 | });
69 | }
70 |
71 | getStartValue(): ITemplate {
72 | return {
73 | id: "",
74 | replaceKey: "",
75 | replaceTeamKey: "",
76 | text: "",
77 | description: "",
78 | gitUrl: "",
79 | user: "",
80 | pass: "",
81 | branch: "",
82 | tags: []
83 | };
84 | }
85 |
86 | close(that: this) {
87 | that.selection.clear();
88 | that.setState({
89 | showAuthentication: false,
90 | currentTemplate: this.getStartValue(),
91 | tagSuggestions: StackValues
92 | }, () => {
93 | that.props.onDismiss();
94 | });
95 | }
96 |
97 | reset(that: this) {
98 | that.selection.clear();
99 | that.setState({
100 | showAuthentication: false,
101 | currentTemplate: that.getStartValue(),
102 | tagSuggestions: StackValues
103 | })
104 | }
105 |
106 | onInputChange(event: React.ChangeEvent, value: string, that: this) {
107 | var prop = event.target.id.replace("__bolt-", "");
108 | that.setState(prevState => ({
109 | currentTemplate: { ...prevState.currentTemplate, [prop]: value }
110 | }));
111 | }
112 |
113 | isValid(): boolean {
114 | const { currentTemplate, showAuthentication } = this.state;
115 |
116 | return (
117 | currentTemplate.text && currentTemplate.text.trim() !== "" &&
118 | currentTemplate.description && currentTemplate.description.trim() !== "" &&
119 | currentTemplate.gitUrl && currentTemplate.gitUrl.trim() !== "" &&
120 | currentTemplate.replaceKey && currentTemplate.replaceKey.trim() !== "" &&
121 | currentTemplate.replaceTeamKey && currentTemplate.replaceTeamKey.trim() !== "" &&
122 | currentTemplate.branch && currentTemplate.branch.trim() !== "" &&
123 | currentTemplate.tags && currentTemplate.tags.length > 0 &&
124 | (showAuthentication ? currentTemplate.pass && currentTemplate.pass.trim() !== "" : true)
125 | );
126 | }
127 |
128 | render() {
129 |
130 | const { showAuthentication, currentTemplate, templates, tagSuggestions } = this.state;
131 |
132 | if (this.props.show) {
133 | return (
134 | {
136 | this.close(this);
137 | }}
138 | titleProps={{ text: "Template catalog" }}
139 | description={"Base repository configuration for template generation."}
140 | footerButtonProps={[
141 | {
142 | text: "Close", onClick: (event) => {
143 | this.close(this)
144 | },
145 | }
146 | ]}>
147 |
148 |
149 |
150 | this.onInputChange(event, value, this)}
155 | placeholder="Name your template"
156 | />
157 |
158 |
159 | this.onInputChange(event, value, this)}
163 | required={true}
164 | multiline={true}
165 | rows={4}
166 | placeholder="Template description"
167 | />
168 |
169 |
170 | this.onInputChange(event, value, this)}
175 | required={true}
176 | placeholder="e.g. https://github.com/Microsoft/vscode.git"
177 | />
178 |
179 |
180 | this.onInputChange(event, value, this)}
185 | required={true}
186 | placeholder="Name of the branch. e.g. develop, main"
187 | />
188 |
189 |
190 | this.onInputChange(event, value, this)}
195 | required={true}
196 | placeholder="Replaces every instance of 'replaceKey' with project name"
197 | />
198 |
199 |
200 | this.onInputChange(event, value, this)}
205 | required={true}
206 | placeholder="Replaces every instance of 'replaceTeamKey' with project team Name"
207 | />
208 |
209 |
210 |
213 |
214 |
{
217 | return a.id === b.id;
218 | }}
219 | convertItemToPill={(tag: IStack) => {
220 | return {
221 | content: tag.text
222 | };
223 | }}
224 | onSearchChanged={(searchValue: string) => {
225 | var items =
226 | StackValues.filter(item =>
227 | currentTemplate.tags.findIndex(d => d.id === item.id) === -1
228 | ).filter(
229 | testItem => testItem.text.toLowerCase().indexOf(searchValue.toLowerCase()) > -1
230 | )
231 | this.setState({ tagSuggestions: items });
232 | }}
233 | onTagAdded={(tag: IStack) => {
234 | currentTemplate.tags.push(tag);
235 | this.setState({
236 | currentTemplate: currentTemplate, tagSuggestions: StackValues.filter(item =>
237 | currentTemplate.tags.findIndex(d => d.id === item.id) === -1
238 | )
239 | });
240 | }}
241 | onTagRemoved={(tag: IStack) => {
242 | var items = currentTemplate.tags.filter(x => x.id !== tag.id)
243 | currentTemplate.tags = items;
244 | this.setState({
245 | currentTemplate: currentTemplate, tagSuggestions: StackValues.filter(item =>
246 | items.findIndex(d => d.id === item.id) === -1
247 | )
248 | });
249 | }}
250 | renderSuggestionItem={(tag: ISuggestionItemProps) => {
251 | return {tag.item.text}
;
252 | }}
253 | selectedTags={currentTemplate.tags}
254 | suggestions={tagSuggestions}
255 | suggestionsLoading={false}
256 | />
257 |
258 |
259 |
260 | {
264 | currentTemplate.user = "";
265 | currentTemplate.pass = "";
266 | this.setState({
267 | showAuthentication: value,
268 | currentTemplate: currentTemplate
269 | });
270 | }}
271 | />
272 |
273 | {showAuthentication && <>
274 |
275 | this.onInputChange(event, value, this)}
279 | placeholder="Username"
280 | />
281 |
282 |
283 | this.onInputChange(event, value, this)}
287 | inputType={"password"}
288 | required={true}
289 | placeholder="Password / PAT *"
290 | />
291 |
292 | >}
293 |
294 |
295 | {!currentTemplate.id &&
296 | {
301 | currentTemplate.id = Guid.create().toString();
302 | templates.push(currentTemplate);
303 | this.service.saveTemplate(currentTemplate);
304 | this.reset(this);
305 | }} />
306 | }
307 | {currentTemplate.id &&
308 |
309 | {
313 | this.reset(this);
314 | }} />
315 | {
319 | this.service.removeTemplate(currentTemplate.id);
320 | this.setState({ templates: templates.filter(d => d.id !== currentTemplate.id).sortByProp("text") })
321 | this.reset(this);
322 | }} />
323 | {
328 | let items = templates.filter(d => d.id !== currentTemplate.id);
329 | items.push(currentTemplate);
330 | this.service.saveTemplate(currentTemplate);
331 | this.setState({ templates: items.sortByProp("text") })
332 | this.reset(this);
333 | }} />
334 |
335 | }
336 |
337 |
338 | {templates && templates.length > 0 &&
339 |
340 |
341 |
342 | >(new ArrayItemProvider(templates))}>
343 | {(observableProps: { itemProvider: ArrayItemProvider }) => (
344 | , listRow: IListRow) => {
350 | var items = StackValues.filter(item =>
351 | listRow.data.tags.findIndex(d => d.id === item.id) === -1
352 | )
353 | this.setState({
354 | showAuthentication: listRow.data.pass !== "",
355 | currentTemplate: listRow.data.deepcopy(),
356 | tagSuggestions: items
357 | });
358 | }}
359 | />
360 | )}
361 |
362 |
363 |
364 |
365 | }
366 |
367 |
368 |
369 | );
370 | }
371 | return null;
372 | }
373 |
374 | renderRow = (
375 | index: number,
376 | item: ITemplate,
377 | details: IListItemDetails,
378 | key?: string
379 | ): JSX.Element => {
380 | return (
381 |
382 |
383 |
386 |
{item.text}
387 |
388 | {item.description}
389 |
390 |
391 | {item.gitUrl}
392 |
393 |
394 | {item.tags.map(tag => (
395 | {tag.text}
396 | ))}
397 |
398 |
399 |
400 |
401 | );
402 | };
403 |
404 | }
405 |
406 | export default TemplatePanel;
--------------------------------------------------------------------------------
/front/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom';
2 | import "./services/registration";
3 | import "./utils/extensions.ts";
4 |
5 | import { SurfaceBackground, SurfaceContext } from "azure-devops-ui/Surface";
6 | import App from './app';
7 |
8 | ReactDOM.render(
9 |
10 |
11 | ,
12 | document.getElementById('root')
13 | );
14 |
--------------------------------------------------------------------------------
/front/src/model/api.ts:
--------------------------------------------------------------------------------
1 | export interface IApi {
2 | id?: string;
3 | url: string;
4 | name: string;
5 | }
--------------------------------------------------------------------------------
/front/src/model/buildOptions.ts:
--------------------------------------------------------------------------------
1 | import { IUserContext } from "azure-devops-extension-sdk";
2 | import { ITemplate } from "./template";
3 |
4 | export interface IBuildOptions {
5 | name: string;
6 | team: string;
7 | repositoryId: string;
8 | template: ITemplate;
9 | user: IUserContext;
10 | }
--------------------------------------------------------------------------------
/front/src/model/code.ts:
--------------------------------------------------------------------------------
1 | import { ISonarComponent } from "./sonar";
2 |
3 | export interface ICode {
4 | id?: string;
5 | type: string;
6 | server: string;
7 | token: string;
8 | components?: string[];
9 | }
10 |
11 |
12 |
--------------------------------------------------------------------------------
/front/src/model/project.ts:
--------------------------------------------------------------------------------
1 | import { IUserContext } from "azure-devops-extension-sdk";
2 | import { IStatusProps } from "azure-devops-ui/Status";
3 | import { ITemplate } from "./template";
4 |
5 | export interface IProject {
6 | id: string;
7 | name: string;
8 | team: string;
9 | repoName: string;
10 | status: string;
11 | template?: ITemplate;
12 |
13 | repoUrl?: string;
14 | repoId?: string;
15 |
16 | buildDefinitionId?: number;
17 | runBuildId?: number;
18 | startTime?: Date;
19 | endTime?: Date;
20 |
21 | user?: IUserContext;
22 | }
23 |
24 | export interface IStatusIndicator {
25 | statusProps: IStatusProps;
26 | label: string;
27 | }
28 |
29 | export enum ProjectStatus {
30 | Running = "running",
31 | Succeeded = "succeeded",
32 | Failed = "failed",
33 | Warning = "warning",
34 | }
--------------------------------------------------------------------------------
/front/src/model/sonar.ts:
--------------------------------------------------------------------------------
1 | export interface ISonarComponent {
2 | key: string;
3 | name: string;
4 | branches: ISonarBranch[];
5 | }
6 |
7 | export interface ISonarBranch {
8 | name: string;
9 | isMain: boolean;
10 | type: string;
11 | analysisDate: Date;
12 | status: any;
13 | measures: ISonarMeasure[]
14 | isShow: boolean;
15 | link: string;
16 | }
17 |
18 | export interface ISonarMeasure {
19 | metric: string;
20 | value: string;
21 | }
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/front/src/model/stacks.ts:
--------------------------------------------------------------------------------
1 | import { TestImages } from "@fluentui/example-data";
2 | import { IChoiceGroupOption } from "@fluentui/react";
3 |
4 | export interface IStack {
5 | id: number;
6 | text: string;
7 | }
8 |
9 | export const StackValues: IStack[] = [
10 | {
11 | id: 1,
12 | text: "C#"
13 | },
14 | {
15 | id: 2,
16 | text: "API"
17 | },
18 | {
19 | id: 3,
20 | text: "React"
21 | },
22 | {
23 | id: 4,
24 | text: "Type Script"
25 | },
26 | {
27 | id: 5,
28 | text: "Java"
29 | },
30 | {
31 | id: 6,
32 | text: "Kotlin"
33 | },
34 | {
35 | id: 7,
36 | text: "NLayer"
37 | },
38 | {
39 | id: 8,
40 | text: "VSA"
41 | }
42 | ];
43 |
44 | export const DatabaseValues: IChoiceGroupOption[] = [
45 | {
46 | key: 'bar',
47 | imageSrc: TestImages.choiceGroupBarUnselected,
48 | imageAlt: 'Bar chart icon',
49 | selectedImageSrc: TestImages.choiceGroupBarSelected,
50 | imageSize: { width: 32, height: 32 },
51 | text: 'MongoDB',
52 | },
53 | {
54 | key: 'pie',
55 | imageSrc: TestImages.choiceGroupBarUnselected,
56 | selectedImageSrc: TestImages.choiceGroupBarSelected,
57 | imageSize: { width: 32, height: 32 },
58 | text: 'SQLServer',
59 | },
60 | ];
--------------------------------------------------------------------------------
/front/src/model/template.ts:
--------------------------------------------------------------------------------
1 | import { IStack } from "./stacks";
2 |
3 | export interface ITemplate {
4 | id: string;
5 | text: string;
6 | replaceKey: string;
7 | replaceTeamKey: string;
8 | description: string;
9 | gitUrl: string;
10 | user: string;
11 | pass: string;
12 | branch: string;
13 | tags: IStack[]
14 | }
--------------------------------------------------------------------------------
/front/src/model/user.ts:
--------------------------------------------------------------------------------
1 | export interface IUser {
2 | id: string;
3 | name: string;
4 | userName: string;
5 | }
6 |
--------------------------------------------------------------------------------
/front/src/pages/api-docs/api-page-settings.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { Card } from "azure-devops-ui/Card";
4 | import { IObservableValue } from "azure-devops-ui/Core/Observable";
5 | import { ScreenSize } from "azure-devops-ui/Core/Util/Screen";
6 | import { Header, TitleSize } from "azure-devops-ui/Header";
7 | import { IListItemDetails, List, ListItem, ListSelection } from "azure-devops-ui/List";
8 |
9 | import {
10 | bindSelectionToObservable,
11 | MasterDetailsContext,
12 | } from "azure-devops-ui/MasterDetailsContext";
13 |
14 | import { Page } from "azure-devops-ui/Page";
15 |
16 | import { Tooltip } from "azure-devops-ui/TooltipEx";
17 | import { ArrayItemProvider } from "azure-devops-ui/Utilities/Provider";
18 | import { ScreenSizeObserver } from "azure-devops-ui/Utilities/ScreenSize";
19 |
20 | import SwaggerUI from "swagger-ui-react"
21 | import "swagger-ui-react/swagger-ui.css"
22 |
23 | import { IApi } from "../../model/api";
24 | import Api from "./api-page";
25 |
26 | export const SampleData2: IApi[] = [
27 | {
28 | id: "1",
29 | name: "Company.Service.Push",
30 | url: "https://petstore.swagger.io/v2/swagger.json",
31 | },
32 | {
33 | id: "2",
34 | name: "Company.Service.User",
35 | url: "https://generator.swagger.io/api/swagger.json",
36 | },
37 | {
38 | id: "3",
39 | name: "Company.Service.Report",
40 | url: "https://petstore.swagger.io/v2/swagger.json",
41 | },
42 | ];
43 |
44 | export const ListView: React.FunctionComponent<{
45 | initialSelectedMasterItem: IObservableValue;
46 | initialItems: IApi[];
47 | that: Api;
48 | }> = (props) => {
49 | const [initialItemProvider] = React.useState(new ArrayItemProvider(props.initialItems));
50 | const [initialSelection] = React.useState(new ListSelection({ selectOnFocus: false }));
51 | const masterDetailsContext = React.useContext(MasterDetailsContext);
52 |
53 | React.useEffect(() => {
54 | bindSelectionToObservable(
55 | initialSelection,
56 | initialItemProvider,
57 | props.initialSelectedMasterItem,
58 | );
59 | });
60 |
61 | return (
62 | { masterDetailsContext.setDetailsPanelVisbility(true); props.that.setState({ seletectedApi: props.initialSelectedMasterItem.value }) }}
69 | />
70 | );
71 | };
72 |
73 | export const DetailView: React.FunctionComponent<{
74 | detailItem: IApi;
75 | deleteEvent: any
76 | }> = (props) => {
77 | const masterDetailsContext = React.useContext(MasterDetailsContext);
78 | const { detailItem, deleteEvent } = props;
79 |
80 | return (
81 |
82 |
83 | {(screenSizeProps: { screenSize: ScreenSize }) => {
84 | const showBackButton = screenSizeProps.screenSize <= ScreenSize.small;
85 | return (
86 | masterDetailsContext.setDetailsPanelVisbility(false) } : undefined}
93 | commandBarItems={[
94 | {
95 | id: "delete",
96 | text: "Delete",
97 | important: false,
98 | onActivate: () => {
99 | deleteEvent(detailItem)
100 | }
101 | },
102 | ]}
103 | />
104 | );
105 | }}
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | const renderListRow = (
117 | index: number,
118 | item: IApi,
119 | details: IListItemDetails,
120 | key?: string
121 | ): JSX.Element => {
122 | return (
123 |
129 |
130 |
131 |
132 | {item.name}
133 |
134 |
135 |
136 | {item.url}
137 |
138 |
139 |
140 |
141 |
142 |
143 | );
144 | };
--------------------------------------------------------------------------------
/front/src/pages/api-docs/api-page.scss:
--------------------------------------------------------------------------------
1 | @import "azure-devops-ui/Core/_platformCommon.scss";
2 |
3 | body {
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
7 | sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 |
12 | .api-page {
13 |
14 | &--loading {
15 | margin-top: 50px;
16 | }
17 |
18 | }
19 |
20 |
21 | ////CUSTOM -------------------------------------------------------------------
22 |
23 | button, input, select, textarea {
24 | color: inherit;
25 | font-family: inherit;
26 | font-size: inherit;
27 | }
28 |
29 | .scheme-container{
30 | display: none !important;
31 | }
32 |
33 | .swagger-ui {
34 | width: 100%;
35 | color: $black;
36 | }
37 |
38 | .swagger-ui section.models
39 | {
40 | border: 1px solid $neutral-alpha-80;
41 | }
42 |
43 | .swagger-ui section.models.is-open h4,
44 | .swagger-ui .opblock-tag {
45 | border-bottom: 1px solid $neutral-alpha-80;
46 | }
47 |
48 | .swagger-ui .loading-container .loading:before {
49 | border: 2px solid rgba(85,85,85,.1);
50 | border-top-color: $black !important;
51 | }
52 |
53 | .swagger-ui .loading-container .loading:after,
54 | .swagger-ui .model,
55 | .swagger-ui .tab li,
56 | .swagger-ui .model-title,
57 | .swagger-ui .parameter__type,
58 | .swagger-ui .parameter__name,
59 | .swagger-ui table thead tr td, .swagger-ui table thead tr th,
60 | .swagger-ui .opblock .opblock-summary-description,
61 | .swagger-ui .opblock .opblock-summary-operation-id, .swagger-ui .opblock .opblock-summary-path, .swagger-ui .opblock .opblock-summary-path__deprecated,
62 | .swagger-ui .opblock-tag small
63 | {
64 | color: $neutral-alpha-80 !important;
65 | }
66 |
67 | .swagger-ui section.models h4,
68 | .swagger-ui .opblock-tag,
69 | .swagger-ui .info li,
70 | .swagger-ui .info p,
71 | .swagger-ui .info table,
72 | .swagger-ui .info .base-url,
73 | .swagger-ui .info .title
74 | {
75 | color: $black;
76 | }
77 |
78 | .master-row {
79 | background-color: inherit;
80 | cursor: pointer;
81 | }
82 |
83 | .master-row-content {
84 | padding: 8px 8px 8px 20px;
85 | }
86 |
87 | .master-scroll-container {
88 | max-height: 200px;
89 | width: 100%;
90 | }
91 |
92 | @media screen and (max-width: 1023px) {
93 | .single-layer-details {
94 | max-width: 150px;
95 | }
96 | }
97 |
98 | .single-layer-details-contents {
99 | font-size: 18px;
100 | margin-left: 12px;
101 | }
102 |
103 | .description-primary-text.secondary-text,
104 | .bolt-master-panel-header-subtitle.secondary-text {
105 | color: inherit;
106 | }
107 |
108 | @media screen and (max-width: 800px) {
109 | .context-details {
110 | max-width: 320px;
111 | }
112 | }
113 |
114 | .details-view-title {
115 | white-space: normal;
116 | }
117 |
118 | .bolt-master-panel-header-title {
119 | white-space: normal;
120 | }
121 |
--------------------------------------------------------------------------------
/front/src/pages/api-docs/api-page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './api-page.scss';
3 |
4 | import { Page } from 'azure-devops-ui/Page';
5 | import { BaseMasterDetailsContext, MasterDetailsContext } from 'azure-devops-ui/MasterDetailsContext';
6 | import { DetailsPanel, MasterPanel } from 'azure-devops-ui/MasterDetails';
7 | import { DetailView, ListView } from './api-page-settings';
8 | import { Header, TitleSize } from 'azure-devops-ui/Header';
9 | import { ObservableValue } from 'azure-devops-ui/Core/Observable';
10 | import { IApi } from '../../model/api';
11 | import { ZeroData, ZeroDataActionType } from 'azure-devops-ui/ZeroData';
12 | import { Services } from '../../services/services';
13 | import { ApiServiceId, IApiService } from '../../services/api';
14 | import { Spinner } from "azure-devops-ui/Spinner";
15 |
16 | import ApiModal from '../../components/api-docs/api-modal';
17 | import ApiPanel from '../../components/api-docs/api-panel';
18 |
19 | interface IApiState {
20 | showAdd: boolean;
21 | showDelete: boolean;
22 | loading: boolean;
23 | apis: IApi[];
24 | seletectedApi?: IApi;
25 | }
26 |
27 | class Api extends React.Component<{}, IApiState> {
28 |
29 | apiService = Services.getService(ApiServiceId);
30 |
31 | constructor(props: {}) {
32 | super(props);
33 | this.state = {
34 | showAdd: false,
35 | showDelete: false,
36 | loading: true,
37 | apis: []
38 | };
39 |
40 | this.loadApis();
41 | }
42 |
43 | loadApis() {
44 | this.apiService.getApi().then(apis => {
45 | apis.sortByProp("name");
46 | this.setState({ apis: apis, loading: false });
47 | }).catch(e => {
48 | this.setState({ loading: false });
49 | });
50 | }
51 |
52 | deleteApi(that: this) {
53 | that.apiService.removeApi(that.state.seletectedApi.id).then(() => {
54 | that.setState({ seletectedApi: null, showDelete: false });
55 | that.loadApis();
56 | })
57 | }
58 |
59 | render() {
60 |
61 | const { showAdd, showDelete, loading, apis, seletectedApi } = this.state;
62 |
63 | return (
64 |
65 |
66 | {!loading && apis.length === 0 &&
70 | Centralize all api documentation and save time.
71 |
72 | }
73 | imageAltText="Bars"
74 | imagePath={"https://cdn.vsassets.io/ext/ms.vss-code-web/import-content/repoNotFound.bVoHtlP2mhhyPo5t.svg"}
75 | actionText="Register"
76 | actionType={ZeroDataActionType.ctaButton}
77 | onActionClick={(event, item) =>
78 | this.setState({ showAdd: true })
79 | } />
80 | }
81 |
82 | {!loading && apis.length > 0 && {
96 | this.setState({ showAdd: true });
97 | }
98 | },
99 | ]}
100 | titleSize={TitleSize.Large}
101 | />,
102 | renderContent: (parentItem, initialSelectedMasterItem) => (
103 |
104 | ),
105 | hideBackButton: true,
106 | },
107 | detailsContent: {
108 | renderContent: (item) => { this.setState({ seletectedApi: item, showDelete: true }); }} />,
109 | },
110 | selectedMasterItem: new ObservableValue(seletectedApi ?? apis[0]),
111 | parentItem: undefined,
112 | }, () => { })}>
113 |
114 |
115 |
116 |
117 | }
118 |
119 | {loading &&
120 |
121 |
}
122 |
123 | { this.setState({ showAdd: false, loading: true }); this.loadApis() }} />
124 | { this.setState({ showDelete: false }); }} onConfirm={() => { this.setState({ loading: true }); this.deleteApi(this); }} />
125 |
126 | );
127 | }
128 |
129 |
130 |
131 | }
132 |
133 |
134 | export default Api;
135 |
--------------------------------------------------------------------------------
/front/src/pages/code-quality/code-page-settings.tsx:
--------------------------------------------------------------------------------
1 | import { Status, Statuses, StatusSize } from "azure-devops-ui/Status";
2 | import { AiFillBug } from "react-icons/ai";
3 | import { FaRadiationAlt } from "react-icons/fa";
4 | import { GiCheckedShield, GiPadlock } from "react-icons/gi";
5 | import { ISonarBranch, ISonarMeasure } from "../../model/sonar";
6 |
7 |
8 | export function renderBranchStatus(branch: ISonarBranch, className?: string) {
9 |
10 | if (!branch)
11 | return ;
12 |
13 | if (branch.status.qualityGateStatus === "ERROR")
14 | return ;
15 | else
16 | return ;
17 |
18 | };
19 |
20 | export function configureMeasure(measure: ISonarMeasure, measures: ISonarMeasure[]): any {
21 |
22 | var status: string = "";
23 | var ratings: ISonarMeasure[] = [];
24 |
25 | switch (measure.metric) {
26 |
27 | case "vulnerabilities":
28 | ratings = measures.filter(d => d.metric === "security_rating");
29 | if (ratings.length > 0)
30 | status = ratings[0].value.replace(".0", "");
31 | return {
32 | label: "Vulnerabilities",
33 | value: measure.value,
34 | status: status,
35 | icon:
36 | };
37 |
38 | case "bugs":
39 | ratings = measures.filter(d => d.metric === "reliability_rating");
40 | if (ratings.length > 0)
41 | status = ratings[0].value.replace(".0", "");
42 | return {
43 | label: "Bugs",
44 | value: measure.value,
45 | status: status,
46 | icon:
47 | };
48 |
49 | case "security_hotspots_reviewed":
50 | ratings = measures.filter(d => d.metric === "security_review_rating");
51 | if (ratings.length > 0)
52 | status = ratings[0].value.replace(".0", "");
53 | return {
54 | label: "Hotspots",
55 | value: measure.value + "%",
56 | status: status,
57 | icon:
58 | };
59 |
60 | case "code_smells":
61 | ratings = measures.filter(d => d.metric === "sqale_rating");
62 | if (ratings.length > 0)
63 | status = ratings[0].value.replace(".0", "");
64 | return {
65 | label: "Code Smells",
66 | value: measure.value,
67 | status: status,
68 | icon:
69 | };
70 |
71 | case "coverage":
72 | return {
73 | label: "Coverage",
74 | value: measure.value + "%",
75 | };
76 |
77 |
78 | case "duplicated_lines_density":
79 | return {
80 | label: "Duplications",
81 | value: measure.value + "%",
82 | };
83 |
84 | default:
85 | return null;
86 | }
87 | };
--------------------------------------------------------------------------------
/front/src/pages/code-quality/code-page.scss:
--------------------------------------------------------------------------------
1 | @import "azure-devops-ui/Core/_platformCommon.scss";
2 |
3 | body {
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
7 | sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 |
12 | button, input, select, textarea {
13 | color: inherit;
14 | font-family: inherit;
15 | font-size: inherit;
16 | }
17 |
18 | .code {
19 |
20 | &--number {
21 | color: $black;
22 | float: left;
23 | }
24 |
25 | &--title {
26 | margin: 20px 0 10px -15px;
27 | }
28 |
29 | &--status {
30 | margin-top: 5px !important;
31 | }
32 |
33 | &--tag {
34 |
35 | color: white;
36 | width: 25px;
37 | height: 25px;
38 | margin-top: 20px;
39 | margin-left: 10px;
40 | float: left;
41 |
42 | &--1 {
43 | background-color: green !important;
44 | position: relative;
45 | &::after {
46 | display: block;
47 | content: 'A';
48 | position: absolute;
49 | left: 8px;
50 | }
51 | }
52 |
53 | &--2 {
54 | background-color: #b0d513 !important;
55 | position: relative;
56 | &::after {
57 | display: block;
58 | content: 'B';
59 | position: absolute;
60 | left: 8px;
61 | }
62 | }
63 |
64 | &--3 {
65 | background-color: #eabe06 !important;
66 | position: relative;
67 | &::after {
68 | display: block;
69 | content: 'C';
70 | position: absolute;
71 | left: 8px;
72 | }
73 | }
74 |
75 | &--4 {
76 | background-color: #ed7d20 !important;
77 | position: relative;
78 | &::after {
79 | display: block;
80 | content: 'D';
81 | position: absolute;
82 | left: 8px;
83 | }
84 | }
85 |
86 | &--5 {
87 | background-color: red !important;
88 | position: relative;
89 | &::after {
90 | display: block;
91 | content: 'E';
92 | position: absolute;
93 | left: 8px;
94 | }
95 | }
96 |
97 | }
98 |
99 | }
100 |
101 |
--------------------------------------------------------------------------------
/front/src/pages/code-quality/code-page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './code-page.scss';
3 |
4 | import {
5 | CustomHeader,
6 | HeaderDescription,
7 | HeaderIcon,
8 | HeaderTitle,
9 | HeaderTitleArea,
10 | HeaderTitleRow,
11 | TitleSize
12 | } from "azure-devops-ui/Header";
13 |
14 | import { Button } from 'azure-devops-ui/Button';
15 | import { ButtonGroup } from 'azure-devops-ui/ButtonGroup';
16 | import { Page } from 'azure-devops-ui/Page';
17 | import { Card } from 'azure-devops-ui/Card';
18 | import { Pill } from 'azure-devops-ui/Components/Pill/Pill';
19 |
20 | import { Tab, TabBar, TabSize } from 'azure-devops-ui/Tabs';
21 | import { Services } from '../../services/services';
22 | import { ISonarService, SonarServiceId } from '../../services/sonar';
23 | import { ISonarBranch, ISonarComponent } from '../../model/sonar';
24 | import { Duration } from 'azure-devops-ui/Duration';
25 | import { MessageCard, MessageCardSeverity } from 'azure-devops-ui/MessageCard';
26 | import { configureMeasure, renderBranchStatus, } from './code-page-settings';
27 |
28 | import CodePanel from '../../components/code-quality/code-panel';
29 | import { Link, Spinner } from '@fluentui/react';
30 | import { ZeroData, ZeroDataActionType } from 'azure-devops-ui/ZeroData';
31 | import { CodeServiceId, ICodeService } from '../../services/code';
32 |
33 | interface ICodeState {
34 | settingsExpanded: boolean;
35 | loading: boolean;
36 | components: ISonarComponent[];
37 | selectedTabs: string[];
38 | }
39 |
40 | class Code extends React.Component<{}, ICodeState> {
41 |
42 | sonarService = Services.getService(SonarServiceId);
43 | codeService = Services.getService(CodeServiceId);
44 |
45 | constructor(props: {}) {
46 | super(props);
47 |
48 | this.state = {
49 | settingsExpanded: false,
50 | loading: true,
51 | components: [],
52 | selectedTabs: []
53 | };
54 |
55 | this.loadComponents();
56 |
57 | }
58 |
59 | async loadComponents() {
60 |
61 | this.setState({ components: [], loading: true, settingsExpanded: false });
62 |
63 | var selectedTabs: string[] = [];
64 | var server: string = "";
65 | var token: string = "";
66 | var projects: string[] = [];
67 |
68 | try {
69 |
70 | var configs = await this.codeService.getCode();
71 |
72 | if (configs.length > 0) {
73 | var config = configs[configs.length-1];
74 | server = config.server;
75 | token = config.token;
76 | projects = config.components;
77 | }
78 |
79 | if (projects.length > 0) {
80 |
81 | var components = await this.sonarService.loadProjects(server, token, projects);
82 |
83 | for (let c = 0; c < components.length; c++) {
84 | const component = components[c];
85 | var branches = (await this.sonarService.loadBranches(server, token, component.key)).filter(b => b.analysisDate);
86 |
87 | for (let b = 0; b < branches.length; b++) {
88 | const branch = branches[b];
89 |
90 | if (b === 0)
91 | selectedTabs.push(component.key + "_" + branch.name)
92 |
93 | branch.measures = await this.sonarService.loadMeasures(server, token, component.key, branch.name);
94 | branch.link = server + "/dashboard?branch=" + branch.name + "&id=" + component.key;
95 | }
96 | component.branches = branches;
97 | }
98 | this.setState({ components: components, selectedTabs: selectedTabs, loading: false });
99 | }
100 | else {
101 | this.setState({ loading: false });
102 | }
103 |
104 | } catch (e) {
105 | console.log(e);
106 | this.setState({ loading: false });
107 | }
108 | }
109 |
110 | onSelectedTabChanged = (newTabId: string, componentKey: string) => {
111 |
112 | var selectedTabs = this.state.selectedTabs.filter(t => !t.startsWith(componentKey))
113 | selectedTabs.push(newTabId);
114 |
115 | this.setState({ selectedTabs: selectedTabs });
116 | };
117 |
118 | getCurrentBranch = (component: ISonarComponent): ISonarBranch => {
119 |
120 | var selectedTab = this.state.selectedTabs.filter(t => t.startsWith(component.key))[0]
121 | if (selectedTab) {
122 | var branchName = selectedTab.replace(component.key + "_", "");
123 | return component.branches.filter(b => b.name === branchName)[0];
124 | }
125 | return component.branches[0];
126 | };
127 |
128 | render() {
129 |
130 | const { settingsExpanded, components, selectedTabs, loading } = this.state;
131 |
132 | return (
133 |
134 |
135 |
136 |
137 |
138 |
139 | Code quality
140 |
141 |
142 |
143 | Integration with code review and analysis tools (Sonarqube)
144 |
145 |
146 |
147 | this.setState({ settingsExpanded: true })}
149 | />
150 |
151 |
152 |
153 |
154 |
155 | {!loading && components.length === 0 &&
159 | Integrate with code review and analysis tools (Sonarqube).
160 |
161 | }
162 | imageAltText="Bars"
163 | imagePath={"https://cdn.vsassets.io/ext/ms.vss-code-web/import-content/repoNotFound.bVoHtlP2mhhyPo5t.svg"}
164 | actionText="Register"
165 | actionType={ZeroDataActionType.ctaButton}
166 | onActionClick={(event, item) =>
167 | this.setState({ settingsExpanded: true })
168 | } />
169 | }
170 |
171 | {loading &&
172 |
173 |
}
174 |
175 | {components.map((component, index) => {
176 |
177 | var currentBranch = this.getCurrentBranch(component);
178 |
179 | return (
180 |
181 |
182 | {
186 | return renderBranchStatus(currentBranch, className);
187 | }
188 | }}
189 | titleSize={TitleSize.Large}
190 | />
191 |
192 |
193 |
194 |
198 | {component.name}
199 |
200 |
201 |
202 |
203 |
204 |
205 | {component.branches.length > 0 &&
206 |
207 |
208 | Last analysis:
209 | ago
213 |
214 |
215 | { this.onSelectedTabChanged(newTabId, component.key) }}
217 | selectedTabId={selectedTabs.filter(t => t.startsWith(component.key))[0]}
218 | tabSize={TabSize.Compact}
219 | >
220 | {component.branches.map((branch, index) => (
221 |
222 | ))}
223 |
224 |
225 |
226 |
227 | }
228 |
229 | {component.branches.length === 0 &&
230 |
231 | No analysis was performed
232 |
233 | }
234 |
235 |
236 |
237 |
238 |
239 |
240 | {component.branches.length === 0 &&
241 |
245 | There is no branch configured for this project
246 |
247 | }
248 |
249 | {component.branches.map((branch, index) => {
250 | return (selectedTabs.filter(t => t.startsWith(component.key + "_" + branch.name)).length > 0 &&
251 |
252 | {branch.measures.sortByProp("metric").map((measure, index) => {
253 | var item = configureMeasure(measure, branch.measures);
254 |
255 | return (item != null &&
256 |
257 |
{item.icon} {item.label}
258 |
259 |
260 | {item.value}
261 |
262 | {item.status &&
}
263 |
264 |
265 | )
266 | })}
267 |
268 | )
269 | })}
270 |
271 |
)
272 |
273 |
274 | })}
275 |
276 |
277 | { this.loadComponents() }} />
278 |
279 | );
280 | }
281 | }
282 |
283 | export default Code;
284 |
--------------------------------------------------------------------------------
/front/src/pages/project/projects-page-settings.tsx:
--------------------------------------------------------------------------------
1 | import { Status, Statuses, StatusSize } from "azure-devops-ui/Status";
2 |
3 | import {
4 | ITableColumn,
5 | SimpleTableCell,
6 | TwoLineTableCell,
7 | } from "azure-devops-ui/Table";
8 |
9 | import { css } from "azure-devops-ui/Util";
10 | import { Icon, IIconProps } from "azure-devops-ui/Icon";
11 |
12 | import { Ago } from "azure-devops-ui/Ago";
13 | import { Duration } from "azure-devops-ui/Duration";
14 | import { Pill, PillSize } from "azure-devops-ui/Pill";
15 |
16 | import { IProject, IStatusIndicator, ProjectStatus } from "../../model/project";
17 | import { PillGroup } from "azure-devops-ui/PillGroup";
18 | import { Link } from "azure-devops-ui/Link";
19 | import { VssPersona } from "azure-devops-ui/VssPersona";
20 |
21 |
22 | export var columns: ITableColumn[] = [
23 | {
24 | id: "Status",
25 | name: "Status",
26 | renderCell: renderStatusColumn,
27 | readonly: true,
28 | width: 60,
29 | },
30 | {
31 | id: "name",
32 | name: "Project",
33 | renderCell: renderNameColumn,
34 | readonly: true,
35 | width: -20,
36 | },
37 | {
38 | className: "pipelines-two-line-cell",
39 | id: "repository",
40 | name: "Repository",
41 | renderCell: renderTemplateColumn,
42 | width: -40,
43 | },
44 | {
45 | id: "type",
46 | ariaLabel: "Stack",
47 | readonly: true,
48 | renderCell: renderTagsColumn,
49 | width: -20,
50 | }
51 | // {
52 | // id: "time",
53 | // ariaLabel: "Time and duration",
54 | // readonly: true,
55 | // renderCell: renderDateColumn,
56 | // width: -20,
57 | // }
58 | ];
59 |
60 | export function getStatusIndicator(status: string): IStatusIndicator {
61 | status = status || "";
62 | status = status.toLowerCase();
63 | const indicatorData: IStatusIndicator = {
64 | label: "Success",
65 | statusProps: { ...Statuses.Success, ariaLabel: "Success" },
66 | };
67 | switch (status) {
68 | case ProjectStatus.Failed:
69 | indicatorData.statusProps = { ...Statuses.Failed, ariaLabel: "Failed" };
70 | indicatorData.label = "Failed";
71 | break;
72 | case ProjectStatus.Running:
73 | indicatorData.statusProps = { ...Statuses.Running, ariaLabel: "Running" };
74 | indicatorData.label = "Running";
75 | break;
76 | case ProjectStatus.Warning:
77 | indicatorData.statusProps = { ...Statuses.Warning, ariaLabel: "Warning" };
78 | indicatorData.label = "Warning";
79 |
80 | break;
81 | }
82 |
83 | return indicatorData;
84 | }
85 |
86 | export function renderStatusColumn(
87 | rowIndex: number,
88 | columnIndex: number,
89 | tableColumn: ITableColumn,
90 | tableItem: IProject
91 | ): JSX.Element {
92 | return (
93 |
98 |
103 |
104 |
105 | );
106 | }
107 |
108 | export function renderNameColumn(
109 | rowIndex: number,
110 | columnIndex: number,
111 | tableColumn: ITableColumn,
112 | tableItem: IProject
113 | ): JSX.Element {
114 | return (
115 |
121 | {tableItem.name}
122 |
123 | }
124 | line2={
125 |
126 | {Icon({
127 | className: "icon-margin",
128 | iconName: "FileCode",
129 | key: "release-type",
130 | })}
131 |
132 | {tableItem.template.text}
133 |
134 |
135 | }
136 | />
137 | );
138 | }
139 |
140 | export function renderTemplateColumn(
141 | rowIndex: number,
142 | columnIndex: number,
143 | tableColumn: ITableColumn,
144 | tableItem: IProject
145 | ): JSX.Element {
146 | return (
147 |
154 |
159 | {tableItem.repoName}
160 |
161 |
162 | }
163 | line2={
164 |
165 | {Icon({
166 | className: "icon-margin",
167 | iconName: "Contact",
168 | key: "branch-code",
169 | })}
170 | Generated by
171 |
180 |
181 |
187 | {Icon({
188 | className: "icon-margin",
189 | iconName: "OpenSource",
190 | key: "branch-name",
191 | })}
192 | develop
193 |
194 |
195 | }
196 | />
197 | );
198 | }
199 |
200 | export function renderTagsColumn(
201 | rowIndex: number,
202 | columnIndex: number,
203 | tableColumn: ITableColumn,
204 | tableItem: IProject
205 | ): JSX.Element {
206 | return (
207 |
213 |
214 | {tableItem.template.tags.map(tag => (
215 | {tag.text}
216 | ))}
217 |
218 |
219 |
220 | );
221 | }
222 |
223 | export function renderDateColumn(
224 | rowIndex: number,
225 | columnIndex: number,
226 | tableColumn: ITableColumn,
227 | tableItem: IProject
228 | ): JSX.Element {
229 | return (
230 |
239 | ),
240 | })}
241 | line2={WithIcon({
242 | className: "fontSize font-size bolt-table-two-line-cell-item",
243 | iconProps: { iconName: "Clock" },
244 | children: (
245 |
249 | ),
250 | })}
251 | />
252 | );
253 | }
254 |
255 | function WithIcon(props: {
256 | className?: string;
257 | iconProps: IIconProps;
258 | children?: React.ReactNode;
259 | }) {
260 | return (
261 |
262 | {Icon({ ...props.iconProps, className: "icon-margin" })}
263 | {props.children}
264 |
265 | );
266 | }
267 |
268 |
--------------------------------------------------------------------------------
/front/src/pages/project/projects-page.scss:
--------------------------------------------------------------------------------
1 | @import "azure-devops-ui/Core/_platformCommon.scss";
2 |
3 | body {
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
7 | sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 |
12 | button, input, select, textarea {
13 | color: inherit;
14 | font-family: inherit;
15 | font-size: inherit;
16 | }
17 |
18 | .projects {
19 |
20 | &--add-button {
21 | text-align: right;
22 | float: right;
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/front/src/pages/project/projects-page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './projects-page.scss';
3 |
4 | import {
5 | CustomHeader,
6 | HeaderDescription,
7 | HeaderTitle,
8 | HeaderTitleArea,
9 | HeaderTitleRow,
10 | TitleSize
11 | } from "azure-devops-ui/Header";
12 |
13 | import { DeleteRepositoryAsync } from '../../services/repository';
14 |
15 | import { Card } from "azure-devops-ui/Card";
16 | import { Page } from "azure-devops-ui/Page";
17 | import { Button } from "azure-devops-ui/Button";
18 | import { ButtonGroup } from "azure-devops-ui/ButtonGroup";
19 |
20 | import { ColumnMore, Table } from "azure-devops-ui/Table";
21 | import { ArrayItemProvider } from "azure-devops-ui/Utilities/Provider";
22 | import { ObservableArray, ObservableValue } from "azure-devops-ui/Core/Observable";
23 | import { Observer } from "azure-devops-ui/Observer";
24 | import { Services } from '../../services/services';
25 | import { ITemplateService, TemplateServiceId, } from '../../services/template';
26 | import { ITemplate } from '../../model/template';
27 | import { IProjectService, ProjectServiceId } from '../../services/project';
28 | import { IProject, ProjectStatus } from '../../model/project';
29 | import { ZeroData, ZeroDataActionType } from "azure-devops-ui/ZeroData";
30 | import { columns } from './projects-page-settings';
31 | import { DeletePipelineAsync, GetBuildStatusAsync } from '../../services/pipeline';
32 |
33 | import ProjectPanel from '../../components/project/project-panel';
34 | import TemplatePanel from '../../components/template/template-panel';
35 | import ProjectModal from '../../components/project/project-modal';
36 |
37 |
38 | interface IProjectsState {
39 | templateExpanded: boolean;
40 | projectExpanded: boolean;
41 | templates: ITemplate[];
42 | projects: IProject[];
43 | loading: boolean;
44 | showDelete: boolean;
45 | seletectedProject?: IProject;
46 | }
47 |
48 | class ProjectsPage extends React.Component<{}, IProjectsState> {
49 |
50 | templateService = Services.getService(TemplateServiceId);
51 | projectService = Services.getService(ProjectServiceId);
52 | intervalStatus: any;
53 |
54 | constructor(props: {}) {
55 | super(props);
56 |
57 | this.state = {
58 | showDelete: false,
59 | loading: true,
60 | templateExpanded: false,
61 | projectExpanded: false,
62 | templates: [],
63 | projects: [],
64 | };
65 |
66 | columns.push(new ColumnMore((listItem) => {
67 | return {
68 | id: "sub-menu",
69 | items: [
70 | { id: "delete", text: "Delete", onActivate: () => this.setState({ showDelete: true, seletectedProject: listItem }) },
71 | ],
72 | };
73 | }))
74 |
75 | this.loadProjects();
76 | }
77 |
78 | loadProjects() {
79 | this.templateService.getTemplate().then(templates => {
80 | this.setState({ templates: templates});
81 | this.projectService.getProject().then(projects => {
82 | console.log(projects);
83 | console.log("projects");
84 | this.setState({ projects: projects, loading: false });
85 | this.setVerifyProjectStatus();
86 | }).catch(e => {
87 | this.setState({ loading: false });
88 | });
89 | }).catch(e => {
90 | this.setState({ loading: false });
91 | });
92 | }
93 |
94 | async setVerifyProjectStatus() {
95 |
96 | var that = this;
97 | var projects = that.state.projects;
98 |
99 | await that.verifyProjectStatus(projects, that);
100 |
101 | if (projects.filter(d => d.status === ProjectStatus.Running).length > 0) {
102 | that.intervalStatus = setInterval(async function () {
103 | await that.verifyProjectStatus(projects, that);
104 | }, 5000);
105 | }
106 | }
107 |
108 | async verifyProjectStatus(projects, that: this) {
109 | for (let index = 0; index < projects.length; index++) {
110 | const element = projects[index];
111 | if (element.status === ProjectStatus.Running) {
112 | element.status = await GetBuildStatusAsync(element.runBuildId)
113 | if (element.status === ProjectStatus.Succeeded) {
114 | await DeletePipelineAsync(element.buildDefinitionId);
115 | await this.projectService.updateProject(element);
116 | }
117 | }
118 | }
119 | if (projects.filter(d => d.status === ProjectStatus.Running).length === 0) {
120 | clearInterval(that.intervalStatus);
121 | }
122 | that.setState({ projects: projects });
123 | }
124 |
125 | async deleteProject(type: string, that: this) {
126 |
127 | if (type === "all") {
128 | await DeleteRepositoryAsync(that.state.seletectedProject.repoId);
129 | await DeletePipelineAsync(that.state.seletectedProject.buildDefinitionId);
130 | }
131 |
132 | that.projectService.removeProject(that.state.seletectedProject.id).then(() => {
133 | that.setState({ seletectedProject: null, showDelete: false });
134 | that.loadProjects();
135 | })
136 | }
137 |
138 | render() {
139 |
140 | const { projects, loading, templateExpanded, templates, projectExpanded, seletectedProject, showDelete } = this.state;
141 |
142 | return (
143 |
144 |
145 |
146 |
147 |
148 | Projects
149 |
150 |
151 |
152 | Projects list generated from templates
153 |
154 |
155 |
156 | this.setState({ projectExpanded: true })}
158 | />
159 | this.setState({ templateExpanded: true })}
161 | />
162 |
163 |
164 |
165 |
166 |
167 | {!loading && projects.length === 0 &&
171 | Save time by creating a new project from templates.
172 |
173 | }
174 | imageAltText="Bars"
175 | imagePath={"https://cdn.vsassets.io/ext/ms.vss-code-web/import-content/repoNotFound.bVoHtlP2mhhyPo5t.svg"}
176 | actionText="Create"
177 | actionType={ZeroDataActionType.ctaButton}
178 | onActionClick={(event, item) =>
179 | this.setState({ projectExpanded: true })
180 | } />
181 | }
182 |
183 | {!loading && projects.length > 0 &&
188 | >(
189 | new ArrayItemProvider(this.state.projects)
190 | )}>
191 | {(observableProps: { itemProvider: ArrayItemProvider }) => (
192 |
198 | )}
199 |
200 | }
201 |
202 | {loading &&
207 |
208 | ariaLabel="Table shimmer"
209 | className="table-example"
210 | columns={columns}
211 | containerClassName="h-scroll-auto"
212 | itemProvider={new ObservableArray<
213 | IProject | ObservableValue
214 | >(new Array(5).fill(new ObservableValue(undefined)))}
215 | role="table"
216 | />
217 | }
218 |
219 |
220 |
221 | { this.setState({ templateExpanded: false, loading: true }); this.loadProjects() }} />
222 | { this.setState({ projectExpanded: false, loading: false }); this.loadProjects() }} />
223 | { this.setState({ seletectedProject: null, showDelete: false }); }} onConfirm={(type) => { this.deleteProject(type, this); }} />
224 |
225 |
226 | );
227 | }
228 | }
229 |
230 | export default ProjectsPage;
231 |
--------------------------------------------------------------------------------
/front/src/pages/radar/radar.scss:
--------------------------------------------------------------------------------
1 | @import "azure-devops-ui/Core/_platformCommon.scss";
2 |
3 | body {
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
7 | sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 |
12 | button, input, select, textarea {
13 | color: inherit;
14 | font-family: inherit;
15 | font-size: inherit;
16 | }
--------------------------------------------------------------------------------
/front/src/pages/radar/radar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './radar.scss';
3 |
4 | import {
5 | CustomHeader,
6 | HeaderTitle,
7 | HeaderTitleArea,
8 | HeaderTitleRow,
9 | TitleSize
10 | } from "azure-devops-ui/Header";
11 |
12 | import { Button } from 'azure-devops-ui/Button';
13 | import { ButtonGroup } from 'azure-devops-ui/ButtonGroup';
14 | import { Page } from 'azure-devops-ui/Page';
15 |
16 | interface IRadarState {
17 | }
18 |
19 | class Radar extends React.Component<{}, IRadarState> {
20 |
21 |
22 | render() {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 | Tech Radar
30 |
31 |
32 |
33 |
34 | this.setState({ templateExpanded: true })}
36 | />
37 | this.setState({ settingsExpanded: true })}
39 | />
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 | }
52 |
53 | export default Radar;
54 |
--------------------------------------------------------------------------------
/front/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/front/src/services/api.ts:
--------------------------------------------------------------------------------
1 | import * as DevOps from "azure-devops-extension-sdk";
2 | import {
3 | IExtensionDataManager,
4 | IProjectPageService
5 | } from "azure-devops-extension-api";
6 |
7 | import {
8 | IApi,
9 | } from "../model/api";
10 |
11 | import { getStorageManager } from "./storage";
12 | import { IService } from "./services";
13 |
14 | export interface IApiService extends IService {
15 | getApi(): Promise;
16 | saveApi(api: IApi): Promise;
17 | removeApi(id: string): Promise;
18 | }
19 |
20 | export const ApiServiceId = "ApiService";
21 |
22 | export class ApiService implements IApiService {
23 | manager: IExtensionDataManager | undefined;
24 |
25 | constructor() {
26 | this.getManager();
27 | }
28 |
29 | async getApi(): Promise {
30 | const manager = await this.getManager();
31 |
32 | try {
33 | return manager.getDocuments(await this._getCollection(), {
34 | defaultValue: []
35 | });
36 | } catch {
37 | return [];
38 | }
39 | }
40 |
41 | async saveApi(api: IApi): Promise {
42 | const manager = await this.getManager();
43 | await manager.setDocument(await this._getCollection(), api);
44 | return api;
45 | }
46 |
47 | async removeApi(id: string): Promise {
48 | const manager = await this.getManager();
49 | try {
50 | await manager.deleteDocument(await this._getCollection(), id);
51 | } catch {
52 | // Ignore
53 | }
54 | }
55 |
56 | async getManager(): Promise {
57 | if (!this.manager) {
58 | this.manager = await getStorageManager();
59 | }
60 | return this.manager;
61 | }
62 |
63 | async _getCollection(): Promise {
64 | const apiCollection = "BaseApiCollections";
65 | const apiPageService = await DevOps.getService(
66 | "ms.vss-tfs-web.tfs-page-data-service"
67 | );
68 | const apiInfo = await apiPageService.getProject();
69 | return `${apiCollection}-${apiInfo.id}`;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/front/src/services/code.ts:
--------------------------------------------------------------------------------
1 | import * as DevOps from "azure-devops-extension-sdk";
2 | import {
3 | IExtensionDataManager,
4 | IProjectPageService,
5 | } from "azure-devops-extension-api";
6 |
7 | import { ICode } from "../model/code";
8 |
9 | import { getStorageManager } from "./storage";
10 | import { IService } from "./services";
11 |
12 | export interface ICodeService extends IService {
13 | getCode(): Promise;
14 | saveCode(code: ICode): Promise;
15 | removeCode(id: string): Promise;
16 | removeAll(): Promise;
17 | }
18 |
19 | export const CodeServiceId = "CodeService";
20 |
21 | export class CodeService implements ICodeService {
22 | manager: IExtensionDataManager | undefined;
23 |
24 | constructor() {
25 | this.getManager();
26 | }
27 |
28 | async getCode(): Promise {
29 | const manager = await this.getManager();
30 |
31 | try {
32 | return manager.getDocuments(await this._getCollection(), {
33 | defaultValue: [],
34 | });
35 | } catch {
36 | return [];
37 | }
38 | }
39 |
40 | async saveCode(code: ICode): Promise {
41 | const manager = await this.getManager();
42 | await manager.setDocument(await this._getCollection(), code);
43 | return code;
44 | }
45 |
46 | async removeCode(id: string): Promise {
47 | const manager = await this.getManager();
48 | try {
49 | await manager.deleteDocument(await this._getCollection(), id);
50 | } catch {
51 | // Ignore
52 | }
53 | }
54 |
55 | async removeAll(): Promise {
56 | const manager = await this.getManager();
57 | try {
58 | const documents = await manager.getDocuments(await this._getCollection());
59 | for (let index = 0; index < documents.length; index++) {
60 | const element = documents[index];
61 | await manager.deleteDocument(await this._getCollection(), element.id);
62 | }
63 | } catch {
64 | // Ignore
65 | }
66 | }
67 |
68 | async getManager(): Promise {
69 | if (!this.manager) {
70 | this.manager = await getStorageManager();
71 | }
72 | return this.manager;
73 | }
74 |
75 | async _getCollection(): Promise {
76 | const codeCollection = "BaseCodeCollections";
77 | const codePageService = await DevOps.getService(
78 | "ms.vss-tfs-web.tfs-page-data-service"
79 | );
80 | const codeInfo = await codePageService.getProject();
81 | return `${codeCollection}-${codeInfo.id}`;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/front/src/services/pipeline.ts:
--------------------------------------------------------------------------------
1 | import * as DevOps from "azure-devops-extension-sdk";
2 | import { IProjectPageService } from "azure-devops-extension-api";
3 |
4 | import { getClient } from "azure-devops-extension-api";
5 | import {
6 | AgentPoolQueue,
7 | AgentSpecification,
8 | Build,
9 | BuildDefinition,
10 | BuildDefinitionStep,
11 | BuildDefinitionVariable,
12 | BuildRepository,
13 | BuildRestClient,
14 | BuildResult,
15 | BuildStatus,
16 | DefinitionType,
17 | DesignerProcess,
18 | DesignerProcessTarget,
19 | Phase,
20 | TaskAgentPoolReference,
21 | TaskDefinitionReference,
22 | } from "azure-devops-extension-api/Build";
23 | import { IBuildOptions } from "../model/buildOptions";
24 | import { ProjectStatus } from "../model/project";
25 |
26 | const client: BuildRestClient = getClient(BuildRestClient);
27 |
28 | export interface PhaseTargetScript {
29 | type: number;
30 | allowScriptsAuthAccessOption: boolean;
31 | }
32 |
33 | export async function CreateBuildDefinitionAsync(
34 | options: IBuildOptions
35 | ): Promise {
36 | const projectService = await DevOps.getService(
37 | "ms.vss-tfs-web.tfs-page-data-service"
38 | );
39 | const currentProject = await projectService.getProject();
40 |
41 | const repository = {} as BuildRepository;
42 | repository.type = "TfsGit";
43 | repository.id = options.repositoryId;
44 | repository.defaultBranch = "refs/heads/main";
45 |
46 | const agent = {} as AgentSpecification;
47 | agent.identifier = "ubuntu-20.04";
48 |
49 | const target = {} as DesignerProcessTarget;
50 | target.agentSpecification = agent;
51 |
52 | const phaseTarget = {} as PhaseTargetScript;
53 | phaseTarget.type = 1;
54 | phaseTarget.allowScriptsAuthAccessOption = true;
55 |
56 | const task = {} as TaskDefinitionReference;
57 | task.id = "3d60564b-919d-40f0-aef7-b1703706b8ef";
58 | task.versionSpec = "0.*";
59 | task.definitionType = "task";
60 |
61 | const step = {} as BuildDefinitionStep;
62 | step.task = task;
63 | step.displayName = "Stack Board Repos";
64 | step.enabled = true;
65 | step.inputs = {
66 | sourceRepository: options.template.gitUrl,
67 | branch: options.template.branch,
68 | replaceFrom: options.template.replaceKey,
69 | replaceTo: options.name,
70 | replaceTeamFrom: options.template.replaceTeamKey,
71 | replaceTeamTo: options.team,
72 | };
73 |
74 | const phase = {} as Phase;
75 | phase.name = "Agent job 1";
76 | phase.refName = "Job_1";
77 | phase.condition = "succeeded()";
78 | phase.jobAuthorizationScope = 1;
79 | phase.target = phaseTarget;
80 | phase.steps = [step];
81 |
82 | const designerProcess = {} as DesignerProcess;
83 | designerProcess.type = 1;
84 | designerProcess.target = target;
85 | designerProcess.phases = [phase];
86 |
87 | const taskAgentPoolReference = {} as TaskAgentPoolReference;
88 | taskAgentPoolReference.isHosted = true;
89 | taskAgentPoolReference.name = "Azure Pipelines";
90 |
91 | const agentPoolQueue = {} as AgentPoolQueue;
92 | agentPoolQueue.pool = taskAgentPoolReference;
93 | agentPoolQueue.name = "Azure Pipelines";
94 |
95 | const definition = {} as BuildDefinition;
96 | definition.name = `STACKBOARD-REPOS-${options.name.toUpperCase()}`;
97 | definition.type = DefinitionType.Build;
98 | definition.repository = repository;
99 | definition.process = designerProcess;
100 | definition.queue = agentPoolQueue;
101 |
102 | const PAT = {} as BuildDefinitionVariable;
103 | PAT.isSecret = true;
104 | PAT.value = options.template.pass;
105 |
106 | const userName = {} as BuildDefinitionVariable;
107 | userName.isSecret = true;
108 | userName.value = options.template.user;
109 |
110 | const userInfo = {} as BuildDefinitionVariable;
111 | userInfo.isSecret = true;
112 | userInfo.value = `${options.user.name}|${options.user.displayName}`;
113 |
114 | definition.variables = {
115 | stackboard_pat: PAT,
116 | stackboard_username: userName,
117 | stackboard_userinfo: userInfo,
118 | };
119 |
120 | return await client.createDefinition(definition, currentProject.name);
121 | }
122 |
123 | export async function RunBuildAsync(buildDefinitionId: number): Promise {
124 | const projectService = await DevOps.getService(
125 | "ms.vss-tfs-web.tfs-page-data-service"
126 | );
127 | const currentProject = await projectService.getProject();
128 |
129 | const build = {} as Build;
130 | build.definition = await client.getDefinition(
131 | currentProject.name,
132 | buildDefinitionId
133 | );
134 |
135 | if (build.definition) {
136 | return await client.queueBuild(build, currentProject.name);
137 | }
138 |
139 | throw new Error(`Can't find build definition with id - ${buildDefinitionId}`);
140 | }
141 |
142 | export async function DeletePipelineAsync(
143 | buildDefinitionId: number
144 | ): Promise {
145 | const projectService = await DevOps.getService(
146 | "ms.vss-tfs-web.tfs-page-data-service"
147 | );
148 |
149 | const currentProject = await projectService.getProject();
150 | return await client.deleteDefinition(currentProject.name, buildDefinitionId);
151 | }
152 |
153 | export async function GetBuildStatusAsync(
154 | buildId: number
155 | ): Promise {
156 | try {
157 | const projectService = await DevOps.getService(
158 | "ms.vss-tfs-web.tfs-page-data-service"
159 | );
160 |
161 | const currentProject = await projectService.getProject();
162 | const build = await client.getBuild(currentProject.name, buildId);
163 |
164 | if (build == null) {
165 | return ProjectStatus.Succeeded;
166 | }
167 |
168 | switch (build.status) {
169 | case BuildStatus.None:
170 | case BuildStatus.InProgress:
171 | case BuildStatus.NotStarted:
172 | return ProjectStatus.Running;
173 | case BuildStatus.Cancelling:
174 | return ProjectStatus.Failed;
175 | case BuildStatus.Completed: {
176 | return build.result === BuildResult.Succeeded
177 | ? ProjectStatus.Succeeded
178 | : ProjectStatus.Failed;
179 | }
180 | default:
181 | return ProjectStatus.Running;
182 | }
183 | } catch (error) {
184 | return ProjectStatus.Succeeded;
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/front/src/services/project.ts:
--------------------------------------------------------------------------------
1 | import * as DevOps from "azure-devops-extension-sdk";
2 | import {
3 | IExtensionDataManager,
4 | IProjectPageService
5 | } from "azure-devops-extension-api";
6 |
7 | import {
8 | IProject,
9 | } from "../model/project";
10 |
11 | import { getStorageManager } from "./storage";
12 | import { IService } from "./services";
13 |
14 | export interface IProjectService extends IService {
15 | getProject(): Promise;
16 | saveProject(project: IProject): Promise;
17 | updateProject(project: IProject): Promise;
18 | removeProject(id: string): Promise;
19 | }
20 |
21 | export const ProjectServiceId = "ProjectService";
22 |
23 | export class ProjectService implements IProjectService {
24 | manager: IExtensionDataManager | undefined;
25 |
26 | constructor() {
27 | this.getManager();
28 | }
29 |
30 | async getProject(): Promise {
31 | const manager = await this.getManager();
32 |
33 | try {
34 | return manager.getDocuments(await this._getCollection(), {
35 | defaultValue: []
36 | });
37 | } catch {
38 | return [];
39 | }
40 | }
41 |
42 | async saveProject(project: IProject): Promise {
43 | const manager = await this.getManager();
44 | await manager.setDocument(await this._getCollection(), project);
45 | return project;
46 | }
47 |
48 | async updateProject(project: IProject): Promise {
49 | const manager = await this.getManager();
50 | try {
51 | await manager.updateDocument(await this._getCollection(), project);
52 | } catch {
53 | // Ignore
54 | }
55 | }
56 |
57 | async removeProject(id: string): Promise {
58 | const manager = await this.getManager();
59 | try {
60 | await manager.deleteDocument(await this._getCollection(), id);
61 | } catch {
62 | // Ignore
63 | }
64 | }
65 |
66 | async getManager(): Promise {
67 | if (!this.manager) {
68 | this.manager = await getStorageManager();
69 | }
70 | return this.manager;
71 | }
72 |
73 | async _getCollection(): Promise {
74 | const projectCollection = "BaseProjectCollections";
75 | const projectPageService = await DevOps.getService(
76 | "ms.vss-tfs-web.tfs-page-data-service"
77 | );
78 | const projectInfo = await projectPageService.getProject();
79 | return `${projectCollection}-${projectInfo.id}`;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/front/src/services/registration.ts:
--------------------------------------------------------------------------------
1 | import { Services } from "./services";
2 | import { TemplateService, TemplateServiceId } from "./template";
3 | import { ProjectService, ProjectServiceId } from "./project";
4 | import { ApiService, ApiServiceId } from "./api";
5 | import { CodeService, CodeServiceId } from "./code";
6 | import { SonarService, SonarServiceId } from "./sonar";
7 |
8 | Services.registerService(TemplateServiceId, TemplateService);
9 | Services.registerService(ProjectServiceId, ProjectService);
10 | Services.registerService(ApiServiceId, ApiService);
11 | Services.registerService(CodeServiceId, CodeService);
12 | Services.registerService(SonarServiceId, SonarService);
13 |
--------------------------------------------------------------------------------
/front/src/services/repository.ts:
--------------------------------------------------------------------------------
1 | import * as DevOps from "azure-devops-extension-sdk";
2 | import { IProjectPageService } from "azure-devops-extension-api";
3 |
4 | import { getClient } from "azure-devops-extension-api";
5 | import {
6 | GitChange,
7 | GitCommitRef,
8 | GitItem,
9 | GitPush,
10 | GitRefUpdate,
11 | GitRepository,
12 | GitRepositoryCreateOptions,
13 | GitRestClient,
14 | ItemContent,
15 | ItemContentType,
16 | VersionControlChangeType,
17 | } from "azure-devops-extension-api/Git";
18 |
19 | const client: GitRestClient = getClient(GitRestClient);
20 |
21 | export interface RepoChange {
22 | path: string;
23 | }
24 |
25 | export async function CreateRepositoryAsync(
26 | name: string
27 | ): Promise {
28 |
29 | const projectService = await DevOps.getService(
30 | "ms.vss-tfs-web.tfs-page-data-service"
31 | );
32 | const currentProject = await projectService.getProject();
33 |
34 | const options = {} as GitRepositoryCreateOptions;
35 | options.name = name;
36 |
37 | var repository = await client.createRepository(options, currentProject.name);
38 |
39 | var gitRefUpdate = {} as GitRefUpdate;
40 | gitRefUpdate.name = "refs/heads/main";
41 | gitRefUpdate.oldObjectId = "0000000000000000000000000000000000000000";
42 |
43 | var item = {} as GitItem;
44 | item.path = "/README.md";
45 |
46 | var itemContent = {} as ItemContent;
47 | itemContent.content = itemContent.content = "### Made with Stack Board Extensions";
48 | itemContent.contentType = ItemContentType.RawText;
49 |
50 | var change = {} as GitChange;
51 | change.changeType = VersionControlChangeType.Add;
52 | change.item = item;
53 | change.newContent = itemContent;
54 |
55 | var gitCommitRef = {} as GitCommitRef;
56 | gitCommitRef.comment = "Initial repository made with Stack Board Extensions!";
57 | gitCommitRef.changes = [change];
58 |
59 | var push = {} as GitPush;
60 | push.refUpdates = [gitRefUpdate];
61 | push.commits = [gitCommitRef];
62 |
63 | await client.createPush(push, repository.id, currentProject.name);
64 |
65 | return repository;
66 | }
67 |
68 | export async function DeleteRepositoryAsync(
69 | id: string
70 | ): Promise {
71 |
72 | const projectService = await DevOps.getService(
73 | "ms.vss-tfs-web.tfs-page-data-service"
74 | );
75 |
76 | const currentProject = await projectService.getProject();
77 | return await client.deleteRepository(id, currentProject.name);
78 | }
--------------------------------------------------------------------------------
/front/src/services/services.ts:
--------------------------------------------------------------------------------
1 | export interface IService {}
2 |
3 | export interface IServiceRegistry {
4 | registerService(id: string, service: IService): void;
5 | getService(id: string): TService;
6 | }
7 |
8 | class ServiceRegistry implements IServiceRegistry {
9 | private services: { [id: string]: new () => IService } = {};
10 | private servicesInstances: { [id: string]: IService } = {};
11 |
12 | public registerService(id: string, service: new () => IService): void {
13 | this.services[id] = service;
14 | }
15 |
16 | public getService(id: string): TService {
17 | if (!this.servicesInstances[id]) {
18 | if (!this.services[id]) {
19 | throw new Error(`Can't find service with id ${id}`);
20 | }
21 |
22 | this.servicesInstances[id] = new this.services[id]();
23 | }
24 |
25 | return this.servicesInstances[id] as TService;
26 | }
27 | }
28 |
29 | export const Services: IServiceRegistry = new ServiceRegistry();
30 |
--------------------------------------------------------------------------------
/front/src/services/sonar.ts:
--------------------------------------------------------------------------------
1 | import { IService } from "./services";
2 | import { ISonarBranch, ISonarComponent, ISonarMeasure } from "../model/sonar";
3 |
4 | export interface ISonarService extends IService {
5 | loadComponents(token: string, serverUrl: string): Promise;
6 | loadBranches(serverUrl: string, token: string, project: string): Promise;
7 | loadMeasures(serverUrl: string, token: string, component: string, branch: string): Promise;
8 | loadProjects(serverUrl: string, token: string, projects: string[]): Promise;
9 | }
10 |
11 | export const SonarServiceId = "SonarService";
12 |
13 | export class SonarService implements ISonarService {
14 |
15 |
16 | async loadProjects(serverUrl: string, token: string, projects: string[]): Promise {
17 |
18 | let base64 = require('base-64');
19 | var projectsString = projects.join(",");
20 |
21 | return new Promise((resolve: (results: ISonarComponent[]) => void, reject: (error: any) => void): void => {
22 |
23 | fetch(serverUrl + "/api/projects/search?ps=100&projects=" + projectsString, {
24 | method: "GET",
25 | mode: 'cors',
26 | headers: {
27 | "Authorization": "Basic " + base64.encode(token + ":")
28 | }
29 | })
30 | .then(res => res.json())
31 | .then((data: any) => {
32 | resolve(data.components);
33 | })
34 | .catch(function (error) {
35 | reject(error);
36 | });
37 |
38 | });
39 |
40 | }
41 |
42 | async loadComponents(serverUrl: string, token: string): Promise {
43 |
44 | let base64 = require('base-64');
45 |
46 | return new Promise((resolve: (results: ISonarComponent[]) => void, reject: (error: any) => void): void => {
47 |
48 | fetch(serverUrl + "/api/components/search_projects?ps=100", {
49 | method: "GET",
50 | mode: 'cors',
51 | headers: {
52 | "Authorization": "Basic " + base64.encode(token + ":")
53 | }
54 | })
55 | .then(res => res.json())
56 | .then((data: any) => {
57 | resolve(data.components);
58 | })
59 | .catch(function (error) {
60 | reject(error);
61 | });
62 |
63 | });
64 |
65 | }
66 |
67 | async loadBranches(serverUrl: string, token: string, project: string): Promise {
68 |
69 | let base64 = require('base-64');
70 |
71 | return new Promise((resolve: (results: ISonarBranch[]) => void, reject: (error: any) => void): void => {
72 |
73 | fetch(serverUrl + "/api/project_branches/list?project=" + project, {
74 | method: "GET",
75 | mode: 'cors',
76 | headers: {
77 | "Authorization": "Basic " + base64.encode(token + ":")
78 | }
79 | })
80 | .then(res => res.json())
81 | .then((data: any) => {
82 | resolve(data.branches);
83 | })
84 | .catch(function (error) {
85 | reject(error);
86 | });
87 |
88 | });
89 |
90 | }
91 |
92 | async loadMeasures(serverUrl: string, token: string, component: string, branch: string): Promise {
93 |
94 | let base64 = require('base-64');
95 |
96 | return new Promise((resolve: (results: ISonarMeasure[]) => void, reject: (error: any) => void): void => {
97 |
98 | var url = serverUrl + "/api/measures/component?component=" + component + "&metricKeys=alert_status%2Cbugs%2Creliability_rating%2Cvulnerabilities%2Csecurity_rating%2Csecurity_hotspots_reviewed%2Csecurity_review_rating%2Ccode_smells%2Csqale_rating%2Cduplicated_lines_density%2Ccoverage%2Cncloc%2Cncloc_language_distribution%2Cprojects&branch=" + encodeURIComponent(branch);
99 | fetch(url, {
100 | method: "GET",
101 | mode: 'cors',
102 | headers: {
103 | "Authorization": "Basic " + base64.encode(token + ":")
104 | }
105 | })
106 | .then(res => res.json())
107 | .then((data: any) => {
108 | resolve(data.component.measures);
109 | })
110 | .catch(function (error) {
111 | reject(error);
112 | });
113 |
114 | });
115 |
116 | }
117 |
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/front/src/services/storage.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IExtensionDataManager,
3 | IExtensionDataService
4 | } from "azure-devops-extension-api";
5 | import * as DevOps from "azure-devops-extension-sdk";
6 | import {
7 | getAccessToken,
8 | getExtensionContext,
9 | getService
10 | } from "azure-devops-extension-sdk";
11 |
12 | export async function getStorageManager(): Promise {
13 | await DevOps.ready();
14 | const context = getExtensionContext();
15 | const extensionDataService = await getService(
16 | "ms.vss-features.extension-data-service"
17 | );
18 | const accessToken = await getAccessToken();
19 | return extensionDataService.getExtensionDataManager(
20 | context.id,
21 | accessToken
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/front/src/services/template.ts:
--------------------------------------------------------------------------------
1 | import * as DevOps from "azure-devops-extension-sdk";
2 | import {
3 | IExtensionDataManager,
4 | IProjectPageService
5 | } from "azure-devops-extension-api";
6 |
7 | import {
8 | ITemplate,
9 | } from "../model/template";
10 |
11 | import { getStorageManager } from "./storage";
12 | import { IService } from "./services";
13 |
14 | export interface ITemplateService extends IService {
15 | getTemplate(): Promise;
16 | saveTemplate(template: ITemplate): Promise;
17 | removeTemplate(id: string): Promise;
18 | }
19 |
20 | export const TemplateServiceId = "TemplateService";
21 |
22 | export class TemplateService implements ITemplateService {
23 | manager: IExtensionDataManager | undefined;
24 |
25 | constructor() {
26 | this.getManager();
27 | }
28 |
29 | async getTemplate(): Promise {
30 | const manager = await this.getManager();
31 |
32 | try {
33 | return manager.getDocuments(await this._getCollection(), {
34 | defaultValue: []
35 | });
36 | } catch {
37 | return [];
38 | }
39 | }
40 |
41 | async saveTemplate(template: ITemplate): Promise {
42 | const manager = await this.getManager();
43 | await manager.setDocument(await this._getCollection(), template);
44 | return template;
45 | }
46 |
47 | async removeTemplate(id: string): Promise {
48 | const manager = await this.getManager();
49 | try {
50 | await manager.deleteDocument(await this._getCollection(), id);
51 | } catch {
52 | // Ignore
53 | }
54 | }
55 |
56 | async getManager(): Promise {
57 | if (!this.manager) {
58 | this.manager = await getStorageManager();
59 | }
60 | return this.manager;
61 | }
62 |
63 | async _getCollection(): Promise {
64 | const templateCollection = "BaseTemplateCollections";
65 | const projectPageService = await DevOps.getService(
66 | "ms.vss-tfs-web.tfs-page-data-service"
67 | );
68 | const projectInfo = await projectPageService.getProject();
69 | return `${templateCollection}-${projectInfo.id}`;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/front/src/utils/extensions.ts:
--------------------------------------------------------------------------------
1 | export { };
2 |
3 | declare global {
4 | interface Array {
5 | sortByProp(prop: string): Array;
6 | }
7 | interface Object {
8 | deepcopy(): any;
9 | }
10 | }
11 |
12 | // eslint-disable-next-line
13 | Array.prototype.sortByProp = function (prop: string) {
14 | var _self = this as Array;
15 | try {
16 | return _self.sort((n1, n2) => {
17 | if (n1[prop].toLowerCase() > n2[prop].toLowerCase()) { return 1; }
18 | if (n1[prop].toLowerCase() < n2[prop].toLowerCase()) { return -1; } return 0;
19 | })
20 | }
21 | catch
22 | {
23 | return _self;
24 | }
25 | };
26 |
27 | // eslint-disable-next-line
28 | Object.prototype.deepcopy = function (): any {
29 | var _self = this;
30 | var copy;
31 |
32 | if (null == _self || "object" != typeof _self) return _self;
33 |
34 | if (_self instanceof Date) {
35 | copy = new Date();
36 | copy.setTime(_self.getTime());
37 | return copy;
38 | }
39 |
40 | if (_self instanceof Array) {
41 | copy = [];
42 | for (var i = 0, len = _self.length; i < len; i++) {
43 | copy[i] = _self[i].deepcopy();
44 | }
45 | return copy;
46 | }
47 |
48 | if (_self instanceof Object) {
49 | copy = {};
50 | for (var attr in _self) {
51 | if (_self.hasOwnProperty(attr)) copy[attr] = _self[attr].deepcopy();
52 | }
53 | return copy;
54 | }
55 |
56 | throw new Error("Unable to copy obj! Its type isn't supported.");
57 | };
--------------------------------------------------------------------------------
/front/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "esModuleInterop": true,
7 | "allowSyntheticDefaultImports": true,
8 | "strict": false,
9 | "forceConsistentCasingInFileNames": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 | "lib": [
18 | "dom",
19 | "dom.iterable",
20 | "esnext"
21 | ]
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "lockfileVersion": 1
3 | }
4 |
--------------------------------------------------------------------------------
/tasks/stack-board-replaces/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llima/stack-board-extension/0855274f0f26bee127ddf814c5e163c48598ffe7/tasks/stack-board-replaces/icon.png
--------------------------------------------------------------------------------
/tasks/stack-board-replaces/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stack-board-replaces",
3 | "version": "0.0.1",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/concat-stream": {
8 | "version": "1.6.1",
9 | "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz",
10 | "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==",
11 | "requires": {
12 | "@types/node": "*"
13 | },
14 | "dependencies": {
15 | "@types/node": {
16 | "version": "16.7.8",
17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.8.tgz",
18 | "integrity": "sha512-8upnoQU0OPzbIkm+ZMM0zCeFCkw2s3mS0IWdx0+AAaWqm4fkBb0UJp8Edl7FVKRamYbpJC/aVsHpKWBIbiC7Zg=="
19 | }
20 | }
21 | },
22 | "@types/form-data": {
23 | "version": "0.0.33",
24 | "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
25 | "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=",
26 | "requires": {
27 | "@types/node": "*"
28 | },
29 | "dependencies": {
30 | "@types/node": {
31 | "version": "16.7.8",
32 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.8.tgz",
33 | "integrity": "sha512-8upnoQU0OPzbIkm+ZMM0zCeFCkw2s3mS0IWdx0+AAaWqm4fkBb0UJp8Edl7FVKRamYbpJC/aVsHpKWBIbiC7Zg=="
34 | }
35 | }
36 | },
37 | "@types/node": {
38 | "version": "12.20.21",
39 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.21.tgz",
40 | "integrity": "sha512-Qk7rOvV2A4vNgXNS88vEvbJE1NDFPCQ8AU+pNElrU2bA4yrRDef3fg3SUe+xkwyin3Bpg/Xh5JkNWTlsOcS2tA==",
41 | "dev": true
42 | },
43 | "@types/qs": {
44 | "version": "6.9.7",
45 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
46 | "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
47 | },
48 | "ansi-regex": {
49 | "version": "5.0.0",
50 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
51 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
52 | },
53 | "ansi-styles": {
54 | "version": "4.3.0",
55 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
56 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
57 | "requires": {
58 | "color-convert": "^2.0.1"
59 | }
60 | },
61 | "asap": {
62 | "version": "2.0.6",
63 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
64 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
65 | },
66 | "asynckit": {
67 | "version": "0.4.0",
68 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
69 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
70 | },
71 | "azure-pipelines-task-lib": {
72 | "version": "3.1.8",
73 | "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-3.1.8.tgz",
74 | "integrity": "sha512-33FlyFnom0vnutfT7B2HmSE2d15mwr9XPXFlWCY7kUUF4Ry4xtRabU+6dRDPWtsC8DDgubN3c5erQaRCkG9Y7Q==",
75 | "requires": {
76 | "minimatch": "3.0.4",
77 | "mockery": "^1.7.0",
78 | "q": "^1.5.1",
79 | "semver": "^5.1.0",
80 | "shelljs": "^0.8.4",
81 | "sync-request": "6.1.0",
82 | "uuid": "^3.0.1"
83 | }
84 | },
85 | "balanced-match": {
86 | "version": "1.0.2",
87 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
88 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
89 | },
90 | "brace-expansion": {
91 | "version": "1.1.11",
92 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
93 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
94 | "requires": {
95 | "balanced-match": "^1.0.0",
96 | "concat-map": "0.0.1"
97 | }
98 | },
99 | "buffer-from": {
100 | "version": "1.1.2",
101 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
102 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
103 | },
104 | "call-bind": {
105 | "version": "1.0.2",
106 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
107 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
108 | "requires": {
109 | "function-bind": "^1.1.1",
110 | "get-intrinsic": "^1.0.2"
111 | }
112 | },
113 | "caseless": {
114 | "version": "0.12.0",
115 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
116 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
117 | },
118 | "chalk": {
119 | "version": "4.1.2",
120 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
121 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
122 | "requires": {
123 | "ansi-styles": "^4.1.0",
124 | "supports-color": "^7.1.0"
125 | }
126 | },
127 | "cliui": {
128 | "version": "7.0.4",
129 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
130 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
131 | "requires": {
132 | "string-width": "^4.2.0",
133 | "strip-ansi": "^6.0.0",
134 | "wrap-ansi": "^7.0.0"
135 | }
136 | },
137 | "color-convert": {
138 | "version": "2.0.1",
139 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
140 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
141 | "requires": {
142 | "color-name": "~1.1.4"
143 | }
144 | },
145 | "color-name": {
146 | "version": "1.1.4",
147 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
148 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
149 | },
150 | "combined-stream": {
151 | "version": "1.0.8",
152 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
153 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
154 | "requires": {
155 | "delayed-stream": "~1.0.0"
156 | }
157 | },
158 | "concat-map": {
159 | "version": "0.0.1",
160 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
161 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
162 | },
163 | "concat-stream": {
164 | "version": "1.6.2",
165 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
166 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
167 | "requires": {
168 | "buffer-from": "^1.0.0",
169 | "inherits": "^2.0.3",
170 | "readable-stream": "^2.2.2",
171 | "typedarray": "^0.0.6"
172 | }
173 | },
174 | "core-util-is": {
175 | "version": "1.0.2",
176 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
177 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
178 | },
179 | "delayed-stream": {
180 | "version": "1.0.0",
181 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
182 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
183 | },
184 | "emoji-regex": {
185 | "version": "8.0.0",
186 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
187 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
188 | },
189 | "escalade": {
190 | "version": "3.1.1",
191 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
192 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
193 | },
194 | "form-data": {
195 | "version": "2.5.1",
196 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
197 | "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
198 | "requires": {
199 | "asynckit": "^0.4.0",
200 | "combined-stream": "^1.0.6",
201 | "mime-types": "^2.1.12"
202 | }
203 | },
204 | "fs.realpath": {
205 | "version": "1.0.0",
206 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
207 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
208 | },
209 | "function-bind": {
210 | "version": "1.1.1",
211 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
212 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
213 | },
214 | "get-caller-file": {
215 | "version": "2.0.5",
216 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
217 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
218 | },
219 | "get-intrinsic": {
220 | "version": "1.1.1",
221 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
222 | "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
223 | "requires": {
224 | "function-bind": "^1.1.1",
225 | "has": "^1.0.3",
226 | "has-symbols": "^1.0.1"
227 | }
228 | },
229 | "get-port": {
230 | "version": "3.2.0",
231 | "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
232 | "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw="
233 | },
234 | "glob": {
235 | "version": "7.1.7",
236 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
237 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
238 | "requires": {
239 | "fs.realpath": "^1.0.0",
240 | "inflight": "^1.0.4",
241 | "inherits": "2",
242 | "minimatch": "^3.0.4",
243 | "once": "^1.3.0",
244 | "path-is-absolute": "^1.0.0"
245 | }
246 | },
247 | "has": {
248 | "version": "1.0.3",
249 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
250 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
251 | "requires": {
252 | "function-bind": "^1.1.1"
253 | }
254 | },
255 | "has-flag": {
256 | "version": "4.0.0",
257 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
258 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
259 | },
260 | "has-symbols": {
261 | "version": "1.0.2",
262 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
263 | "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
264 | },
265 | "http-basic": {
266 | "version": "8.1.3",
267 | "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
268 | "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==",
269 | "requires": {
270 | "caseless": "^0.12.0",
271 | "concat-stream": "^1.6.2",
272 | "http-response-object": "^3.0.1",
273 | "parse-cache-control": "^1.0.1"
274 | }
275 | },
276 | "http-response-object": {
277 | "version": "3.0.2",
278 | "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
279 | "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
280 | "requires": {
281 | "@types/node": "^10.0.3"
282 | },
283 | "dependencies": {
284 | "@types/node": {
285 | "version": "10.17.60",
286 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
287 | "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
288 | }
289 | }
290 | },
291 | "inflight": {
292 | "version": "1.0.6",
293 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
294 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
295 | "requires": {
296 | "once": "^1.3.0",
297 | "wrappy": "1"
298 | }
299 | },
300 | "inherits": {
301 | "version": "2.0.4",
302 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
303 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
304 | },
305 | "interpret": {
306 | "version": "1.4.0",
307 | "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
308 | "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="
309 | },
310 | "is-core-module": {
311 | "version": "2.6.0",
312 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz",
313 | "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==",
314 | "requires": {
315 | "has": "^1.0.3"
316 | }
317 | },
318 | "is-fullwidth-code-point": {
319 | "version": "3.0.0",
320 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
321 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
322 | },
323 | "isarray": {
324 | "version": "1.0.0",
325 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
326 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
327 | },
328 | "mime-db": {
329 | "version": "1.49.0",
330 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
331 | "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA=="
332 | },
333 | "mime-types": {
334 | "version": "2.1.32",
335 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz",
336 | "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==",
337 | "requires": {
338 | "mime-db": "1.49.0"
339 | }
340 | },
341 | "minimatch": {
342 | "version": "3.0.4",
343 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
344 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
345 | "requires": {
346 | "brace-expansion": "^1.1.7"
347 | }
348 | },
349 | "mockery": {
350 | "version": "1.7.0",
351 | "resolved": "https://registry.npmjs.org/mockery/-/mockery-1.7.0.tgz",
352 | "integrity": "sha1-9O3g2HUMHJcnwnLqLGBiniyaHE8="
353 | },
354 | "object-inspect": {
355 | "version": "1.11.0",
356 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz",
357 | "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg=="
358 | },
359 | "once": {
360 | "version": "1.4.0",
361 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
362 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
363 | "requires": {
364 | "wrappy": "1"
365 | }
366 | },
367 | "parse-cache-control": {
368 | "version": "1.0.1",
369 | "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
370 | "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104="
371 | },
372 | "path-is-absolute": {
373 | "version": "1.0.1",
374 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
375 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
376 | },
377 | "path-parse": {
378 | "version": "1.0.7",
379 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
380 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
381 | },
382 | "process-nextick-args": {
383 | "version": "2.0.1",
384 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
385 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
386 | },
387 | "promise": {
388 | "version": "8.1.0",
389 | "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz",
390 | "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==",
391 | "requires": {
392 | "asap": "~2.0.6"
393 | }
394 | },
395 | "q": {
396 | "version": "1.5.1",
397 | "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
398 | "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
399 | },
400 | "qs": {
401 | "version": "6.10.1",
402 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz",
403 | "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==",
404 | "requires": {
405 | "side-channel": "^1.0.4"
406 | }
407 | },
408 | "readable-stream": {
409 | "version": "2.3.7",
410 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
411 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
412 | "requires": {
413 | "core-util-is": "~1.0.0",
414 | "inherits": "~2.0.3",
415 | "isarray": "~1.0.0",
416 | "process-nextick-args": "~2.0.0",
417 | "safe-buffer": "~5.1.1",
418 | "string_decoder": "~1.1.1",
419 | "util-deprecate": "~1.0.1"
420 | }
421 | },
422 | "rechoir": {
423 | "version": "0.6.2",
424 | "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
425 | "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
426 | "requires": {
427 | "resolve": "^1.1.6"
428 | }
429 | },
430 | "replace-in-file": {
431 | "version": "6.2.0",
432 | "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.2.0.tgz",
433 | "integrity": "sha512-Im2AF9G/qgkYneOc9QwWwUS/efyyonTUBvzXS2VXuxPawE5yQIjT/e6x4CTijO0Quq48lfAujuo+S89RR2TP2Q==",
434 | "requires": {
435 | "chalk": "^4.1.0",
436 | "glob": "^7.1.6",
437 | "yargs": "^16.2.0"
438 | }
439 | },
440 | "require-directory": {
441 | "version": "2.1.1",
442 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
443 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
444 | },
445 | "resolve": {
446 | "version": "1.20.0",
447 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
448 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
449 | "requires": {
450 | "is-core-module": "^2.2.0",
451 | "path-parse": "^1.0.6"
452 | }
453 | },
454 | "safe-buffer": {
455 | "version": "5.1.2",
456 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
457 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
458 | },
459 | "semver": {
460 | "version": "5.7.1",
461 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
462 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
463 | },
464 | "shelljs": {
465 | "version": "0.8.4",
466 | "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz",
467 | "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==",
468 | "requires": {
469 | "glob": "^7.0.0",
470 | "interpret": "^1.0.0",
471 | "rechoir": "^0.6.2"
472 | }
473 | },
474 | "side-channel": {
475 | "version": "1.0.4",
476 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
477 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
478 | "requires": {
479 | "call-bind": "^1.0.0",
480 | "get-intrinsic": "^1.0.2",
481 | "object-inspect": "^1.9.0"
482 | }
483 | },
484 | "string-width": {
485 | "version": "4.2.2",
486 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
487 | "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
488 | "requires": {
489 | "emoji-regex": "^8.0.0",
490 | "is-fullwidth-code-point": "^3.0.0",
491 | "strip-ansi": "^6.0.0"
492 | }
493 | },
494 | "string_decoder": {
495 | "version": "1.1.1",
496 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
497 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
498 | "requires": {
499 | "safe-buffer": "~5.1.0"
500 | }
501 | },
502 | "strip-ansi": {
503 | "version": "6.0.0",
504 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
505 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
506 | "requires": {
507 | "ansi-regex": "^5.0.0"
508 | }
509 | },
510 | "supports-color": {
511 | "version": "7.2.0",
512 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
513 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
514 | "requires": {
515 | "has-flag": "^4.0.0"
516 | }
517 | },
518 | "sync-request": {
519 | "version": "6.1.0",
520 | "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz",
521 | "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==",
522 | "requires": {
523 | "http-response-object": "^3.0.1",
524 | "sync-rpc": "^1.2.1",
525 | "then-request": "^6.0.0"
526 | }
527 | },
528 | "sync-rpc": {
529 | "version": "1.3.6",
530 | "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz",
531 | "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==",
532 | "requires": {
533 | "get-port": "^3.1.0"
534 | }
535 | },
536 | "then-request": {
537 | "version": "6.0.2",
538 | "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz",
539 | "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==",
540 | "requires": {
541 | "@types/concat-stream": "^1.6.0",
542 | "@types/form-data": "0.0.33",
543 | "@types/node": "^8.0.0",
544 | "@types/qs": "^6.2.31",
545 | "caseless": "~0.12.0",
546 | "concat-stream": "^1.6.0",
547 | "form-data": "^2.2.0",
548 | "http-basic": "^8.1.1",
549 | "http-response-object": "^3.0.1",
550 | "promise": "^8.0.0",
551 | "qs": "^6.4.0"
552 | },
553 | "dependencies": {
554 | "@types/node": {
555 | "version": "8.10.66",
556 | "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
557 | "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="
558 | }
559 | }
560 | },
561 | "typedarray": {
562 | "version": "0.0.6",
563 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
564 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
565 | },
566 | "typescript": {
567 | "version": "4.4.2",
568 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
569 | "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
570 | "dev": true
571 | },
572 | "util-deprecate": {
573 | "version": "1.0.2",
574 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
575 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
576 | },
577 | "uuid": {
578 | "version": "3.4.0",
579 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
580 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
581 | },
582 | "wrap-ansi": {
583 | "version": "7.0.0",
584 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
585 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
586 | "requires": {
587 | "ansi-styles": "^4.0.0",
588 | "string-width": "^4.1.0",
589 | "strip-ansi": "^6.0.0"
590 | }
591 | },
592 | "wrappy": {
593 | "version": "1.0.2",
594 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
595 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
596 | },
597 | "y18n": {
598 | "version": "5.0.8",
599 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
600 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
601 | },
602 | "yargs": {
603 | "version": "16.2.0",
604 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
605 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
606 | "requires": {
607 | "cliui": "^7.0.2",
608 | "escalade": "^3.1.1",
609 | "get-caller-file": "^2.0.5",
610 | "require-directory": "^2.1.1",
611 | "string-width": "^4.2.0",
612 | "y18n": "^5.0.5",
613 | "yargs-parser": "^20.2.2"
614 | }
615 | },
616 | "yargs-parser": {
617 | "version": "20.2.9",
618 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
619 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="
620 | }
621 | }
622 | }
623 |
--------------------------------------------------------------------------------
/tasks/stack-board-replaces/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stack-board-replaces",
3 | "version": "0.0.1",
4 | "description": "Stack Board Repos Extension for Azure DevOps",
5 | "main": "stackboardreplaces.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/llima/stack-board-extension.git"
9 | },
10 | "author": "IT Team",
11 | "license": "ISC",
12 | "bugs": {
13 | "url": "https://github.com/llima/stack-board-extension/issues"
14 | },
15 | "scripts": {
16 | "build": "npm run package-release",
17 | "clear": "(if exist build rd /q /s build)",
18 | "package-release": "tsc -p . && npm prune -production && npm run copy-files",
19 | "copy-files": "copy icon.png build && copy task.json build && Xcopy node_modules build\\node_modules /E/H/C/I"
20 | },
21 | "homepage": "./",
22 | "dependencies": {
23 | "azure-pipelines-task-lib": "^3.1.3",
24 | "replace-in-file": "^6.2.0"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^12.0.0",
28 | "typescript": "^4.1.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tasks/stack-board-replaces/stackboardreplaces.ts:
--------------------------------------------------------------------------------
1 | import tl = require("azure-pipelines-task-lib/task");
2 | import path = require("path");
3 | import fs = require("fs");
4 |
5 | import {
6 | ReplaceInFileConfig,
7 | replaceInFileSync,
8 | ReplaceResult,
9 | } from "replace-in-file";
10 |
11 | async function replaceContent(
12 | options: ReplaceInFileConfig
13 | ): Promise {
14 | try {
15 | return replaceInFileSync(options);
16 | } catch (error) {
17 | throw error;
18 | }
19 | }
20 |
21 | function transformToRegex(values: string[]): RegExp[] {
22 | const regex = [];
23 |
24 | for (let val of values) {
25 | regex.push(new RegExp(`${val}`, "g"));
26 | }
27 |
28 | return regex;
29 | }
30 |
31 | function transformTo(values: string[]): string[] {
32 | const to = [];
33 |
34 | for (let val of values) {
35 | to.push(val);
36 | }
37 |
38 | return to;
39 | }
40 |
41 | function getFirstNode(obj: any, path: string): any {
42 | let nodes: any[] = [];
43 | path.split(".").reduce(function (prev, curr) {
44 | let item = prev ? prev[curr] : null;
45 | nodes.push(JSON.stringify(item));
46 |
47 | return item;
48 | }, obj);
49 |
50 | return nodes.length > 0 ? nodes[0] : null;
51 | }
52 |
53 | function setFirstNode(obj: any, path: string, value: any): any {
54 | let nodes: any[] = [];
55 | let paths = path.split(".");
56 | let key = paths[paths.length - 1];
57 |
58 | paths.reduce(function (prev, curr) {
59 | let item = prev ? prev[curr] : null;
60 | if (item != null) {
61 | if (curr == key) {
62 | prev[curr] = value;
63 | }
64 | }
65 | nodes.push(JSON.stringify(item));
66 | return item;
67 | }, obj);
68 |
69 | return nodes.length > 0 ? nodes[0] : null;
70 | }
71 |
72 | async function main(): Promise {
73 | try {
74 | tl.setResourcePath(path.join(__dirname, "task.json"));
75 |
76 | const folderPath = tl.getPathInput("folderPath", true) ?? "";
77 | const fileType = tl.getPathInput("fileType", true) ?? "yaml";
78 | const targetFiles = tl.getDelimitedInput("targetFiles", "\n", false);
79 |
80 | const useKeyVault = tl.getBoolInput("useKeyVault", false);
81 | const keyVaultVariables = tl.getDelimitedInput("keyVaultVariables", "\n", false);
82 |
83 | let variables: any[] = [];
84 |
85 | const pipelineVariables = tl.getVariables();
86 | for (let pipelineVariable of pipelineVariables) {
87 | variables.push({ key: pipelineVariable.name, value: pipelineVariable.value })
88 | }
89 |
90 | if (useKeyVault) {
91 | console.log("Use Key Vault...", keyVaultVariables);
92 |
93 | for (let keyVaultVariable of keyVaultVariables) {
94 | let secret = tl.getVariable(keyVaultVariable);
95 | variables.push({ key: keyVaultVariable.replace("--", "."), value: secret })
96 | }
97 | }
98 |
99 | const keys = variables.map((k) => k.key);
100 | const values = variables.map((k) => k.value);
101 |
102 | console.log("Replace files...", targetFiles);
103 |
104 | for (let file of targetFiles) {
105 | if (fileType == "yaml") {
106 | const options: ReplaceInFileConfig = {
107 | files: folderPath + "/" + file,
108 | from: transformToRegex(keys),
109 | to: transformTo(values),
110 | };
111 |
112 | await replaceContent(options);
113 | } else {
114 | const content = fs.readFileSync(folderPath + "/" + file, "utf8");
115 | const app = JSON.parse(content);
116 |
117 | let jsonfy = JSON.stringify(content.replace(/(?:\r\n|\r|\n|\s)/g, ""));
118 |
119 | for (let variable of variables) {
120 | const valueNode = setFirstNode(app, variable.key, variable.value);
121 | const currentNode = getFirstNode(app, variable.key);
122 |
123 | if (valueNode != undefined && currentNode != undefined) {
124 | const from = JSON.stringify(valueNode).replace(/(^\"+|\"+$)/gm, "");
125 | const to = JSON.stringify(currentNode).replace(/(^\"+|\"+$)/gm, "");
126 |
127 | jsonfy = jsonfy.replace(from, to);
128 | }
129 | }
130 |
131 | fs.writeFileSync(folderPath + "/" + file, JSON.parse(jsonfy));
132 | }
133 | }
134 |
135 | tl.setResult(tl.TaskResult.Succeeded, "Task completed!");
136 | } catch (err: any) {
137 | tl.setResult(tl.TaskResult.Failed, err);
138 | }
139 | }
140 |
141 | main().catch((err) => {
142 | tl.setResult(tl.TaskResult.Failed, err);
143 | });
144 |
--------------------------------------------------------------------------------
/tasks/stack-board-replaces/task.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "e73624b0-a7b6-4f80-a237-12c585741992",
3 | "name": "stack-board-replaces",
4 | "friendlyName": "Stack Board Replaces",
5 | "description": "Stack Board Repos Extension for Azure DevOps",
6 | "author": "Luiz Lima",
7 | "helpUrl": "https://github.com/llima/stack-board-extension",
8 | "helpMarkDown": "[Learn more about this task](https://github.com/llima/stack-board-extension/blob/develop/README.md)",
9 | "category": "Utility",
10 | "visibility": ["Build", "Release"],
11 | "demands": [],
12 | "version": {
13 | "Major": "0",
14 | "Minor": "0",
15 | "Patch": "2"
16 | },
17 | "minimumAgentVersion": "0.0.1",
18 | "instanceNameFormat": "Stack Board Replaces",
19 | "inputs": [
20 | {
21 | "name": "folderPath",
22 | "type": "filePath",
23 | "label": "Folder",
24 | "helpMarkDown": "File path to the folder",
25 | "defaultValue": "$(System.DefaultWorkingDirectory)/**/folder",
26 | "required": true
27 | },
28 | {
29 | "name": "fileType",
30 | "type": "pickList",
31 | "label": "File format",
32 | "helpMarkDown": "Provide file format on which substitution has to be perfformed",
33 | "defaultValue": "yaml",
34 | "required": true,
35 | "options": {
36 | "yaml": "YAML",
37 | "json": "JSON"
38 | },
39 | "properties": {
40 | "EditableOptions": "False"
41 | }
42 | },
43 | {
44 | "name": "targetFiles",
45 | "type": "multiLine",
46 | "label": "Target files",
47 | "helpMarkDown": "Provide new line separated list of files to substitute the variable values",
48 | "required": false,
49 | "defaultValue": "",
50 | "visibleRule": "fileType = yaml || fileType = json"
51 | },
52 | {
53 | "name": "useKeyVault",
54 | "type": "boolean",
55 | "label": "Use Key Vault",
56 | "helpMarkDown": "Use Variables from Key Vault.",
57 | "required": false,
58 | "defaultValue": false,
59 | "visibleRule": "fileType = json"
60 | },
61 | {
62 | "name": "keyVaultVariables",
63 | "type": "multiLine",
64 | "label": "Key Vault variables",
65 | "helpMarkDown": "Provide new line separated list of Key Vault variables to substitute the values. e.g. StackBoard--Secret--Connection",
66 | "required": false,
67 | "defaultValue": "",
68 | "visibleRule": "useKeyVault == true"
69 | }
70 | ],
71 | "execution": {
72 | "Node10": {
73 | "target": "stackboardreplaces.js"
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tasks/stack-board-replaces/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
9 | "lib": ["es6"], /* Specify library files to be included in the compilation. */
10 | "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | "outDir": "build", /* Redirect output structure to the directory. */
18 | "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true, /* Enable all strict type-checking options. */
29 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
45 |
46 | /* Module Resolution Options */
47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
51 | // "typeRoots": [], /* List of folders to include type definitions from. */
52 | // "types": [], /* Type declaration files to be included in compilation. */
53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
57 |
58 | /* Source Map Options */
59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
63 |
64 | /* Experimental Options */
65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
67 |
68 | /* Advanced Options */
69 | "resolveJsonModule": true, /* Include modules imported with '.json' extension */
70 | "skipLibCheck": true, /* Skip type checking of declaration files. */
71 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tasks/stack-board-repos/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stack-board-repos",
3 | "version": "0.0.23",
4 | "description": "Stack Board Repos Extension for Azure DevOps",
5 | "main": "stackboardrepos.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/llima/stack-board-extension.git"
9 | },
10 | "author": "IT Team",
11 | "license": "ISC",
12 | "bugs": {
13 | "url": "https://github.com/llima/stack-board-extension/issues"
14 | },
15 | "scripts": {
16 | "build": "npm run package-release",
17 | "clear": "(if exist build rd /q /s build)",
18 | "package-release": "tsc -p . && npm prune -production && npm run copy-files",
19 | "copy-files": "copy task.json build && Xcopy node_modules build\\node_modules /E/H/C/I"
20 | },
21 | "homepage": "./",
22 | "dependencies": {
23 | "azure-pipelines-task-lib": "^3.1.3",
24 | "replace-in-file": "^6.2.0",
25 | "shelljs": "^0.8.4"
26 | },
27 | "devDependencies": {
28 | "@types/node": "^12.0.0",
29 | "@types/shelljs": "^0.8.9",
30 | "typescript": "^4.1.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tasks/stack-board-repos/stackboardrepos.ts:
--------------------------------------------------------------------------------
1 | import tl = require("azure-pipelines-task-lib/task");
2 | import path = require("path");
3 | import fs = require("fs");
4 | import shell = require("shelljs");
5 |
6 | import {
7 | ReplaceInFileConfig,
8 | replaceInFileSync,
9 | ReplaceResult,
10 | } from "replace-in-file";
11 |
12 | async function replaceContent(
13 | options: ReplaceInFileConfig
14 | ): Promise {
15 | try {
16 | return replaceInFileSync(options);
17 | } catch (error) {
18 | throw error;
19 | }
20 | }
21 |
22 | async function renameFiles(
23 | folder: string,
24 | from: string,
25 | to: string
26 | ): Promise {
27 | var files = fs.readdirSync(folder),
28 | f: number,
29 | fileName: string,
30 | path: string,
31 | newPath: string,
32 | file: any;
33 |
34 | for (f = 0; f < files.length; f += 1) {
35 | fileName = files[f];
36 | path = folder + "/" + fileName;
37 | file = fs.statSync(path);
38 | newPath = folder + "/" + replaceTo(fileName, from, to);
39 |
40 | fs.renameSync(path, newPath);
41 | if (file.isDirectory()) {
42 | await renameFiles(newPath, from, to);
43 | }
44 | }
45 | }
46 |
47 | function replaceTo(text: string, from: string, to: string): string {
48 | return text.replace(from, to);
49 | }
50 |
51 | function transformToRegex(value: string, includeUpperCase?: boolean): RegExp[] {
52 | const values = [];
53 | values.push(new RegExp(`${value}`, "g"));
54 |
55 | if (includeUpperCase) {
56 | values.push(new RegExp(`${value.toUpperCase()}`, "g"));
57 | }
58 |
59 | return values;
60 | }
61 |
62 | function transformTo(value: string, includeUpperCase?: boolean): string[] {
63 | const values = [];
64 | values.push(value);
65 |
66 | if (includeUpperCase) {
67 | values.push(value);
68 | }
69 |
70 | return values;
71 | }
72 |
73 | function makeGitUrl(url: string, username: string, pass: string): string {
74 | const type = username && pass ? 1 : !username && pass ? 2 : 0;
75 | if (type === 0) {
76 | return url;
77 | }
78 |
79 | let repo = url.replace("https://", "");
80 | if (repo.includes("@")) {
81 | repo = repo.replace(repo.split("@")[0] + "@", "");
82 | }
83 |
84 | switch (type) {
85 | case 1:
86 | return `https://${username}:${pass}@${repo}`;
87 | case 2:
88 | return `https://${pass}@${repo}`;
89 | default:
90 | return "";
91 | }
92 | }
93 |
94 | async function main(): Promise {
95 | try {
96 | tl.setResourcePath(path.join(__dirname, "task.json"));
97 |
98 | const sourceRepository = tl.getPathInput("sourceRepository", true) ?? "";
99 | const replaceFrom = tl.getPathInput("replaceFrom", true) ?? "";
100 | const replaceTo = tl.getPathInput("replaceTo", true) ?? "";
101 |
102 | const replaceTeamFrom = tl.getPathInput("replaceTeamFrom", true) ?? "";
103 | const replaceTeamTo = tl.getPathInput("replaceTeamTo", true) ?? "";
104 |
105 | const branch = tl.getPathInput("branch", true) ?? "develop";
106 |
107 | const userinfo = tl.getVariable("stackboard_userinfo") ?? "|";
108 | const username = tl.getVariable("stackboard_username") ?? "";
109 | const PAT = tl.getVariable("stackboard_pat") ?? "";
110 |
111 | const workingDirectory = tl.getVariable("System.DefaultWorkingDirectory");
112 | const sourceFolder = "STACKBOARD-REPOS-TEMPLATE";
113 |
114 | const sourceGitUrl = makeGitUrl(sourceRepository, username, PAT);
115 | shell.exec(`git clone ${sourceGitUrl} ${sourceFolder}`);
116 |
117 | console.log("Replace name content...");
118 | const options: ReplaceInFileConfig = {
119 | files: sourceFolder + "/**",
120 | from: transformToRegex(replaceFrom, true),
121 | to: transformTo(replaceTo, true),
122 | };
123 | await replaceContent(options);
124 |
125 | console.log("Rename name files...");
126 | await renameFiles(sourceFolder, replaceFrom, replaceTo);
127 |
128 | console.log("Replace team content...");
129 | const optionsTeam: ReplaceInFileConfig = {
130 | files: sourceFolder + "/**",
131 | from: transformToRegex(replaceTeamFrom, true),
132 | to: transformTo(replaceTeamTo, true),
133 | };
134 | await replaceContent(optionsTeam);
135 |
136 | console.log("Rename team files...");
137 | await renameFiles(sourceFolder, replaceTeamFrom, replaceTeamTo);
138 |
139 | shell.rm("-rf", `${sourceFolder}/.git`);
140 |
141 | shell.mv(`${sourceFolder}/*`, `${workingDirectory}`);
142 | shell.mv(`${sourceFolder}/.*`, `${workingDirectory}`);
143 | shell.rm("-rf", sourceFolder);
144 |
145 | console.log("Apply git changes...");
146 |
147 | shell.exec(`git config user.email \"${userinfo.split("|")[0]}\"`);
148 | shell.exec(`git config user.name \"${userinfo.split("|")[1]}\"`);
149 |
150 | shell.exec(`git checkout -b ${branch}`);
151 | shell.exec("git add --all");
152 | shell.exec("git commit -m \"Initial template made with Stack Board Extensions!\"");
153 | shell.exec(`git push origin ${branch} --force`);
154 |
155 | tl.setResult(tl.TaskResult.Succeeded, "Task completed!");
156 | } catch (err: any) {
157 | tl.setResult(tl.TaskResult.Failed, err);
158 | }
159 | }
160 |
161 | main().catch((err) => {
162 | tl.setResult(tl.TaskResult.Failed, err);
163 | });
164 |
--------------------------------------------------------------------------------
/tasks/stack-board-repos/task.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "3d60564b-919d-40f0-aef7-b1703706b8ef",
3 | "name": "stack-board-repos",
4 | "friendlyName": "Stack Board Repos",
5 | "description": "Stack Board Repos Extension for Azure DevOps",
6 | "author": "Luiz Lima",
7 | "helpUrl": "https://github.com/llima/stack-board-extension",
8 | "helpMarkDown": "[Learn more about this task](https://github.com/llima/stack-board-extension/blob/develop/README.md)",
9 | "category": "Utility",
10 | "visibility": [
11 | "Build"
12 | ],
13 | "demands": [],
14 | "version": {
15 | "Major": "0",
16 | "Minor": "0",
17 | "Patch": "24"
18 | },
19 | "minimumAgentVersion": "0.0.22",
20 | "instanceNameFormat": "Stack Board Repos",
21 | "inputs": [
22 | {
23 | "name": "sourceRepository",
24 | "type": "string",
25 | "label": "Source Repository",
26 | "helpMarkDown": "Enter the source repository. e.g. https://github.com/Microsoft/vscode.git",
27 | "defaultValue": "",
28 | "required": true
29 | },
30 | {
31 | "name": "branch",
32 | "type": "string",
33 | "label": "Branch",
34 | "helpMarkDown": "Enter the name of the branch.",
35 | "defaultValue": "develop",
36 | "required": true
37 | },
38 | {
39 | "name": "replaceFrom",
40 | "type": "string",
41 | "label": "Replace From",
42 | "helpMarkDown": "Enter the keyword to be replaced",
43 | "defaultValue": "",
44 | "required": true
45 | },
46 | {
47 | "name": "replaceTo",
48 | "type": "string",
49 | "label": "Replace To",
50 | "helpMarkDown": "Enter the value to be replaced",
51 | "defaultValue": "",
52 | "required": true
53 | },
54 | {
55 | "name": "replaceTeamFrom",
56 | "type": "string",
57 | "label": "Replace Team From",
58 | "helpMarkDown": "Enter the keyword to be replaced",
59 | "defaultValue": "",
60 | "required": true
61 | },
62 | {
63 | "name": "replaceTeanTo",
64 | "type": "string",
65 | "label": "Replace Tean To",
66 | "helpMarkDown": "Enter the value to be replaced",
67 | "defaultValue": "",
68 | "required": true
69 | }
70 | ],
71 | "execution": {
72 | "Node10": {
73 | "target": "stackboardrepos.js"
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/tasks/stack-board-repos/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
9 | "lib": ["es6"], /* Specify library files to be included in the compilation. */
10 | "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | "outDir": "build", /* Redirect output structure to the directory. */
18 | "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true, /* Enable all strict type-checking options. */
29 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
45 |
46 | /* Module Resolution Options */
47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
51 | // "typeRoots": [], /* List of folders to include type definitions from. */
52 | // "types": [], /* Type declaration files to be included in compilation. */
53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
57 |
58 | /* Source Map Options */
59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
63 |
64 | /* Experimental Options */
65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
67 |
68 | /* Advanced Options */
69 | "resolveJsonModule": true, /* Include modules imported with '.json' extension */
70 | "skipLibCheck": true, /* Skip type checking of declaration files. */
71 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/vss-extension.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifestVersion": 1,
3 | "id": "stack-board",
4 | "publisher": "llima",
5 | "version": "1.0.17",
6 | "name": "Stack Board",
7 | "description": "Stack board extension for Azure DevOps",
8 | "public": true,
9 | "categories": [
10 | "Azure Boards"
11 | ],
12 | "targets": [
13 | {
14 | "id": "Microsoft.VisualStudio.Services"
15 | }
16 | ],
17 | "icons": {
18 | "default": "azure/board.png",
19 | "branding": "azure/board.png"
20 | },
21 | "content": {
22 | "details": {
23 | "path": "README.md"
24 | },
25 | "license": {
26 | "path": "azure/license.md"
27 | }
28 | },
29 | "repository": {
30 | "type": "git",
31 | "url": "https://github.com/llima/stack-board-extension"
32 | },
33 | "links": {
34 | "support": {
35 | "url": "mailto:luiz.lima@live.com"
36 | }
37 | },
38 | "scopes": [
39 | "vso.settings",
40 | "vso.profile",
41 | "vso.project",
42 | "vso.work",
43 | "vso.work_write",
44 | "vso.code_manage",
45 | "vso.build_execute"
46 | ],
47 | "tags": [
48 | "Stack",
49 | "Template",
50 | "API Dock"
51 | ],
52 | "contributions": [
53 | {
54 | "id": "stack-board-feature",
55 | "type": "ms.vss-web.feature",
56 | "description": "Enable/Disable all Stack Board features (Projects, API Docs...)",
57 | "targets": [
58 | "ms.vss-web.managed-features"
59 | ],
60 | "properties": {
61 | "name": "Stack Board",
62 | "hostConfigurable": true,
63 | "defaultState": true,
64 | "hostScopes": [
65 | "project"
66 | ]
67 | }
68 | },
69 | {
70 | "id": "stack-board-project-feature",
71 | "type": "ms.vss-web.feature",
72 | "description": "Enable/Disable Project feature (Stack Board)",
73 | "targets": [
74 | "ms.vss-web.managed-features"
75 | ],
76 | "properties": {
77 | "name": "Stack Board - Projects",
78 | "hostConfigurable": true,
79 | "defaultState": true,
80 | "hostScopes": [
81 | "project"
82 | ]
83 | }
84 | },
85 | {
86 | "id": "stack-board-api-feature",
87 | "type": "ms.vss-web.feature",
88 | "description": "Enable/Disable API Docs feature (Stack Board)",
89 | "targets": [
90 | "ms.vss-web.managed-features"
91 | ],
92 | "properties": {
93 | "name": "Stack Board - API Docs",
94 | "hostConfigurable": true,
95 | "defaultState": true,
96 | "hostScopes": [
97 | "project"
98 | ]
99 | }
100 | },
101 | {
102 | "id": "stack-board-code-feature",
103 | "type": "ms.vss-web.feature",
104 | "description": "Enable/Disable Code Quality feature (Stack Board)",
105 | "targets": [
106 | "ms.vss-web.managed-features"
107 | ],
108 | "properties": {
109 | "name": "Stack Board - Code quality",
110 | "hostConfigurable": true,
111 | "defaultState": true,
112 | "hostScopes": [
113 | "project"
114 | ]
115 | }
116 | },
117 | {
118 | "id": "board-hub-group",
119 | "type": "ms.vss-web.hub-group",
120 | "targets": [
121 | "ms.vss-web.project-hub-groups-collection"
122 | ],
123 | "properties": {
124 | "name": "Stack Board",
125 | "order": 100,
126 | "icon": {
127 | "light": "azure/board.png",
128 | "dark": "azure/board.png"
129 | }
130 | },
131 | "constraints": [
132 | {
133 | "name": "Feature",
134 | "properties": {
135 | "featureId": "llima.stack-board.stack-board-feature"
136 | }
137 | }
138 | ]
139 | },
140 | {
141 | "id": "stack-board-hub",
142 | "type": "ms.vss-web.hub",
143 | "targets": [
144 | "llima.stack-board.board-hub-group"
145 | ],
146 | "properties": {
147 | "name": "Projects",
148 | "order": 1,
149 | "uri": "front/build/index.html",
150 | "icon": {
151 | "light": "azure/projects/icon-light.png",
152 | "dark": "azure/projects/icon-dark.png"
153 | }
154 | },
155 | "constraints": [
156 | {
157 | "name": "Feature",
158 | "properties": {
159 | "featureId": "llima.stack-board.stack-board-project-feature"
160 | }
161 | }
162 | ]
163 | },
164 | {
165 | "id": "api-docs-hub",
166 | "type": "ms.vss-web.hub",
167 | "targets": [
168 | "llima.stack-board.board-hub-group"
169 | ],
170 | "properties": {
171 | "name": "API Docs",
172 | "order": 2,
173 | "uri": "front/build/index.html",
174 | "icon": {
175 | "light": "azure/api/icon-light.png",
176 | "dark": "azure/api/icon-dark.png"
177 | }
178 | },
179 | "constraints": [
180 | {
181 | "name": "Feature",
182 | "properties": {
183 | "featureId": "llima.stack-board.stack-board-api-feature"
184 | }
185 | }
186 | ]
187 | },
188 | {
189 | "id": "code-quality-hub",
190 | "type": "ms.vss-web.hub",
191 | "targets": [
192 | "llima.stack-board.board-hub-group"
193 | ],
194 | "properties": {
195 | "name": "Code Quality",
196 | "order": 2,
197 | "uri": "front/build/index.html",
198 | "icon": {
199 | "light": "azure/code/icon-light.png",
200 | "dark": "azure/code/icon-dark.png"
201 | }
202 | },
203 | "constraints": [
204 | {
205 | "name": "Feature",
206 | "properties": {
207 | "featureId": "llima.stack-board.stack-board-code-feature"
208 | }
209 | }
210 | ]
211 | },
212 | {
213 | "id": "stack-board-repos",
214 | "type": "ms.vss-distributed-task.task",
215 | "targets": [
216 | "ms.vss-distributed-task.tasks"
217 | ],
218 | "properties": {
219 | "name": "tasks/stack-board-repos/build"
220 | }
221 | },
222 | {
223 | "id": "stack-board-replaces",
224 | "type": "ms.vss-distributed-task.task",
225 | "targets": [
226 | "ms.vss-distributed-task.tasks"
227 | ],
228 | "properties": {
229 | "name": "tasks/stack-board-replaces/build"
230 | }
231 | }
232 | ],
233 | "files": [
234 | {
235 | "path": "azure",
236 | "addressable": true
237 | },
238 | {
239 | "path": "front/build",
240 | "addressable": true
241 | },
242 | {
243 | "path": "tasks/stack-board-repos/build"
244 | },
245 | {
246 | "path": "tasks/stack-board-replaces/build"
247 | }
248 | ]
249 | }
--------------------------------------------------------------------------------