mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
Move from github.com/feloy/devfile-builder (#6937)
* Move from github.com/feloy/devfile-builder * Update .github/workflows/ui-e2e.yaml Co-authored-by: Armel Soro <armel@rm3l.org> --------- Co-authored-by: Armel Soro <armel@rm3l.org>
This commit is contained in:
21
.github/workflows/ui-e2e.yaml
vendored
Normal file
21
.github/workflows/ui-e2e.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Dev UI E2E tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'ui'
|
||||
- '.github/workflows/ui-e2e.yaml'
|
||||
|
||||
jobs:
|
||||
cypress-run:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
working-directory: ui
|
||||
start: npm start
|
||||
wait-on: 'npx wait-on --timeout 180000 http://0.0.0.0:4200'
|
||||
@@ -59,7 +59,7 @@ skip_if_only() {
|
||||
}
|
||||
|
||||
skip() {
|
||||
SKIP_IF_ONLY="docs/ CONTRIBUTING.md OWNERS README.md USAGE_DATA.md scripts/ .github/ .threatmodel/"
|
||||
SKIP_IF_ONLY="docs/ CONTRIBUTING.md OWNERS README.md USAGE_DATA.md scripts/ .github/ .threatmodel/ ui/"
|
||||
change=$1
|
||||
for skip in ${SKIP_IF_ONLY}; do
|
||||
if [[ "${change}" == "${skip}"* ]]; then
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# - ignore files in certain directories, like 'vendor' or 'dist' (created when building RPM Packages of odo)
|
||||
# - gofmt doesn't exit with error code when there are errors
|
||||
|
||||
GO_FILES=$(find . \( -path ./vendor -o -path ./dist -o -path ./.ibm/tools/tests-results/vendor \) -prune -o -name '*.go' -print)
|
||||
GO_FILES=$(find . \( -path ./vendor -o -path ./dist -o -path ./.ibm/tools/tests-results/vendor -o -path ./ui/wasm/vendor \) -prune -o -name '*.go' -print)
|
||||
|
||||
for file in $GO_FILES; do
|
||||
gofmtOutput=$(gofmt -l "$file")
|
||||
|
||||
46
ui/.gitignore
vendored
Normal file
46
ui/.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
/cypress/videos
|
||||
|
||||
.odo
|
||||
12
ui/Makefile
Normal file
12
ui/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
build-wasm:
|
||||
( \
|
||||
cd wasm/ && \
|
||||
GOOS=js GOARCH=wasm go build -o ../src/assets/devfile.wasm && \
|
||||
HASH=$$(md5sum ../src/assets/devfile.wasm | awk '{ print $$1 }') && \
|
||||
echo $${HASH} && \
|
||||
mv ../src/assets/devfile.wasm ../src/assets/devfile.$${HASH}.wasm && \
|
||||
sed -i "s/devfile\.[a-z0-9]*\.wasm/devfile\.$${HASH}.wasm/" ../src/app/app.module.ts \
|
||||
)
|
||||
|
||||
deploy:
|
||||
npm run deploy
|
||||
36
ui/README.md
Normal file
36
ui/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Devfile Builder
|
||||
|
||||
Devfile Builder is a tool to help users edit and create Devfile (https://devfile.io).
|
||||
|
||||
## Development with Devfile
|
||||
|
||||
Run `odo dev --platform podman` from this directory to start a development session.
|
||||
|
||||
## Development (generic, from Angular documentation)
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.2.2.
|
||||
|
||||
### Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||
|
||||
### Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
### Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
### Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
### Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
### Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||
|
||||
106
ui/angular.json
Normal file
106
ui/angular.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"devfile-builder": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/devfile-builder",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/CNAME",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/indigo-pink.css",
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
"src/assets/wasm_exec.js"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1500kb",
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "devfile-builder:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "devfile-builder:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "devfile-builder:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/indigo-pink.css",
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"deploy": {
|
||||
"builder": "angular-cli-ghpages:deploy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
ui/cypress.config.ts
Normal file
11
ui/cypress.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
viewportWidth: 1080,
|
||||
viewportHeight: 660,
|
||||
});
|
||||
8
ui/cypress/e2e/consts.ts
Normal file
8
ui/cypress/e2e/consts.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const TAB_YAML = 0;
|
||||
export const TAB_CHART = 1;
|
||||
export const TAB_METADATA = 2;
|
||||
export const TAB_COMMANDS = 3;
|
||||
export const TAB_VOLUMES = 4;
|
||||
export const TAB_CONTAINERS = 5;
|
||||
export const TAB_IMAGES = 6;
|
||||
export const TAB_RESOURCES = 7;
|
||||
133
ui/cypress/e2e/errs.cy.ts
Normal file
133
ui/cypress/e2e/errs.cy.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { TAB_COMMANDS, TAB_CONTAINERS, TAB_IMAGES, TAB_RESOURCES } from "./consts";
|
||||
|
||||
describe('devfile editor errors handling', () => {
|
||||
|
||||
it('fails when YAML is not valid', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
cy.setDevfile("wrong yaml content");
|
||||
cy.getByDataCy("yaml-error").should('contain.text', 'error parsing devfile YAML');
|
||||
});
|
||||
|
||||
it('fails when adding a container with an already used name', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
cy.fixture('input/with-container.yaml').then(yaml => {
|
||||
cy.setDevfile(yaml);
|
||||
});
|
||||
cy.selectTab(TAB_CONTAINERS);
|
||||
cy.getByDataCy('add').click();
|
||||
cy.getByDataCy('container-name').type('container1');
|
||||
cy.getByDataCy('container-image').type('an-image');
|
||||
cy.getByDataCy('container-create').click();
|
||||
cy.on('window:alert', (str) => {
|
||||
expect(str).to.contain(`container1 already exists`)
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when adding an image with an already used name', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
cy.fixture('input/with-container.yaml').then(yaml => {
|
||||
cy.setDevfile(yaml);
|
||||
});
|
||||
cy.selectTab(TAB_IMAGES);
|
||||
cy.getByDataCy('image-name').type('container1');
|
||||
cy.getByDataCy('image-image-name').type('an-image-name');
|
||||
cy.getByDataCy('image-build-context').type('/path/to/build/context');
|
||||
cy.getByDataCy('image-dockerfile-uri').type('/path/to/dockerfile');
|
||||
cy.getByDataCy('image-create').click();
|
||||
cy.on('window:alert', (str) => {
|
||||
expect(str).to.contain(`container1 already exists`)
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when adding a resource with an already used name', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
cy.fixture('input/with-container.yaml').then(yaml => {
|
||||
cy.setDevfile(yaml);
|
||||
});
|
||||
cy.selectTab(TAB_RESOURCES);
|
||||
cy.getByDataCy('resource-name').type('container1');
|
||||
cy.getByDataCy('resource-toggle-inlined').click();
|
||||
cy.getByDataCy('resource-manifest').type('a-resource-manifest');
|
||||
cy.getByDataCy('resource-create').click();
|
||||
cy.on('window:alert', (str) => {
|
||||
expect(str).to.contain(`container1 already exists`)
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when adding an exec command with an already used name', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
cy.fixture('input/with-exec-command.yaml').then(yaml => {
|
||||
cy.setDevfile(yaml);
|
||||
});
|
||||
cy.selectTab(TAB_COMMANDS);
|
||||
cy.getByDataCy('add').click();
|
||||
cy.getByDataCy('new-command-exec').click();
|
||||
|
||||
cy.getByDataCy('command-exec-name').type('command1');
|
||||
cy.getByDataCy('command-exec-command-line').type('a-cmdline');
|
||||
cy.getByDataCy('command-exec-working-dir').type('/path/to/working/dir');
|
||||
cy.getByDataCy('select-container').click().get('mat-option').contains('container1').click();
|
||||
cy.getByDataCy('command-exec-create').click();
|
||||
cy.on('window:alert', (str) => {
|
||||
expect(str).to.contain(`command1 already exists`)
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when adding an apply command with an already used name', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
cy.fixture('input/with-apply-command.yaml').then(yaml => {
|
||||
cy.setDevfile(yaml);
|
||||
});
|
||||
cy.selectTab(TAB_COMMANDS);
|
||||
cy.getByDataCy('add').click();
|
||||
cy.getByDataCy('new-command-apply').click();
|
||||
|
||||
cy.getByDataCy('command-apply-name').type('command1');
|
||||
cy.getByDataCy('select-container').click().get('mat-option').contains('resource1').click();
|
||||
cy.getByDataCy('command-apply-create').click();
|
||||
cy.on('window:alert', (str) => {
|
||||
expect(str).to.contain(`command1 already exists`)
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when adding an image command with an already used name', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
cy.fixture('input/with-image-command.yaml').then(yaml => {
|
||||
cy.setDevfile(yaml);
|
||||
});
|
||||
cy.selectTab(TAB_COMMANDS);
|
||||
cy.getByDataCy('add').click();
|
||||
cy.getByDataCy('new-command-image').click();
|
||||
|
||||
cy.getByDataCy('command-image-name').type('command1');
|
||||
cy.getByDataCy('select-container').click().get('mat-option').contains('image1').click();
|
||||
cy.getByDataCy('command-image-create').click();
|
||||
cy.on('window:alert', (str) => {
|
||||
expect(str).to.contain(`command1 already exists`)
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when adding a composite command with an already used name', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
cy.fixture('input/with-image-command.yaml').then(yaml => {
|
||||
cy.setDevfile(yaml);
|
||||
});
|
||||
cy.selectTab(TAB_COMMANDS);
|
||||
cy.getByDataCy('add').click();
|
||||
cy.getByDataCy('new-command-composite').click();
|
||||
|
||||
cy.getByDataCy('command-composite-name').type('command1');
|
||||
cy.getByDataCy('command-composite-create').click();
|
||||
cy.on('window:alert', (str) => {
|
||||
expect(str).to.contain(`command1 already exists`)
|
||||
});
|
||||
});
|
||||
});
|
||||
207
ui/cypress/e2e/spec.cy.ts
Normal file
207
ui/cypress/e2e/spec.cy.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import {TAB_COMMANDS, TAB_CONTAINERS, TAB_IMAGES, TAB_METADATA, TAB_RESOURCES} from './consts';
|
||||
|
||||
describe('devfile editor spec', () => {
|
||||
|
||||
it('displays matadata.name set in YAML', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
cy.fixture('input/with-metadata-name.yaml').then(yaml => {
|
||||
cy.setDevfile(yaml);
|
||||
});
|
||||
|
||||
cy.selectTab(TAB_METADATA);
|
||||
cy.getByDataCy("metadata-name").should('have.value', 'test-devfile');
|
||||
});
|
||||
|
||||
it('displays container set in YAML', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
cy.fixture('input/with-container.yaml').then(yaml => {
|
||||
cy.setDevfile(yaml);
|
||||
});
|
||||
|
||||
cy.selectTab(TAB_CONTAINERS);
|
||||
cy.getByDataCy('container-info').first()
|
||||
.should('contain.text', 'container1')
|
||||
.should('contain.text', 'nginx')
|
||||
.should('contain.text', 'the command to run')
|
||||
.should('contain.text', 'with arg');
|
||||
});
|
||||
|
||||
it('displays a created container', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
|
||||
cy.selectTab(TAB_CONTAINERS);
|
||||
cy.getByDataCy('container-name').type('created-container');
|
||||
cy.getByDataCy('container-image').type('an-image');
|
||||
cy.getByDataCy('container-create').click();
|
||||
|
||||
cy.getByDataCy('container-info').first()
|
||||
.should('contain.text', 'created-container')
|
||||
.should('contain.text', 'an-image');
|
||||
});
|
||||
|
||||
it('displays a created image', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
|
||||
cy.selectTab(TAB_IMAGES);
|
||||
cy.getByDataCy('image-name').type('created-image');
|
||||
cy.getByDataCy('image-image-name').type('an-image-name');
|
||||
cy.getByDataCy('image-build-context').type('/path/to/build/context');
|
||||
cy.getByDataCy('image-dockerfile-uri').type('/path/to/dockerfile');
|
||||
cy.getByDataCy('image-create').click();
|
||||
|
||||
cy.getByDataCy('image-info').first()
|
||||
.should('contain.text', 'created-image')
|
||||
.should('contain.text', 'an-image-name')
|
||||
.should('contain.text', '/path/to/build/context')
|
||||
.should('contain.text', '/path/to/dockerfile');
|
||||
});
|
||||
|
||||
it('displays a created resource, with manifest', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
|
||||
cy.selectTab(TAB_RESOURCES);
|
||||
cy.getByDataCy('resource-name').type('created-resource');
|
||||
cy.getByDataCy('resource-toggle-inlined').click();
|
||||
cy.getByDataCy('resource-manifest').type('a-resource-manifest');
|
||||
cy.getByDataCy('resource-create').click();
|
||||
|
||||
cy.getByDataCy('resource-info').first()
|
||||
.should('contain.text', 'created-resource')
|
||||
.should('contain.text', 'a-resource-manifest');
|
||||
});
|
||||
|
||||
it('displays a created resource, with uri (default)', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
|
||||
cy.selectTab(TAB_RESOURCES);
|
||||
cy.getByDataCy('resource-name').type('created-resource');
|
||||
cy.getByDataCy('resource-uri').type('/my/manifest.yaml');
|
||||
cy.getByDataCy('resource-create').click();
|
||||
|
||||
cy.getByDataCy('resource-info').first()
|
||||
.should('contain.text', 'created-resource')
|
||||
.should('contain.text', 'URI')
|
||||
.should('contain.text', '/my/manifest.yaml');
|
||||
});
|
||||
|
||||
it('creates an exec command with a new container', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
|
||||
cy.selectTab(TAB_COMMANDS);
|
||||
cy.getByDataCy('add').click();
|
||||
cy.getByDataCy('new-command-exec').click();
|
||||
|
||||
cy.getByDataCy('command-exec-name').type('created-command');
|
||||
cy.getByDataCy('command-exec-command-line').type('a-cmdline');
|
||||
cy.getByDataCy('command-exec-working-dir').type('/path/to/working/dir');
|
||||
cy.getByDataCy('select-container').click().get('mat-option').contains('(New Container)').click();
|
||||
cy.getByDataCy('container-name').type('a-created-container');
|
||||
cy.getByDataCy('container-image').type('an-image');
|
||||
cy.getByDataCy('container-create').click();
|
||||
|
||||
cy.getByDataCy('select-container').should('contain', 'a-created-container');
|
||||
cy.getByDataCy('command-exec-create').click();
|
||||
|
||||
cy.getByDataCy('command-info').first()
|
||||
.should('contain.text', 'created-command')
|
||||
.should('contain.text', 'a-cmdline')
|
||||
.should('contain.text', '/path/to/working/dir')
|
||||
.should('contain.text', 'a-created-container');
|
||||
|
||||
cy.selectTab(TAB_CONTAINERS);
|
||||
cy.getByDataCy('container-info').first()
|
||||
.should('contain.text', 'a-created-container')
|
||||
.should('contain.text', 'an-image');
|
||||
});
|
||||
|
||||
it('creates an apply image command with a new image', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
|
||||
cy.selectTab(TAB_COMMANDS);
|
||||
cy.getByDataCy('add').click();
|
||||
cy.getByDataCy('new-command-image').click();
|
||||
cy.getByDataCy('command-image-name').type('created-command');
|
||||
cy.getByDataCy('select-container').click().get('mat-option').contains('(New Image)').click();
|
||||
cy.getByDataCy('image-name').type('a-created-image');
|
||||
cy.getByDataCy('image-image-name').type('an-image-name');
|
||||
cy.getByDataCy('image-build-context').type('/context/dir');
|
||||
cy.getByDataCy('image-dockerfile-uri').type('/path/to/Dockerfile');
|
||||
cy.getByDataCy('image-create').click();
|
||||
|
||||
cy.getByDataCy('select-container').should('contain', 'a-created-image');
|
||||
cy.getByDataCy('command-image-create').click();
|
||||
|
||||
cy.getByDataCy('command-info').first()
|
||||
.should('contain.text', 'created-command')
|
||||
.should('contain.text', 'a-created-image');
|
||||
|
||||
cy.selectTab(TAB_IMAGES);
|
||||
cy.getByDataCy('image-info').first()
|
||||
.should('contain.text', 'a-created-image')
|
||||
.should('contain.text', 'an-image-name')
|
||||
.should('contain.text', '/context/dir')
|
||||
.should('contain.text', '/path/to/Dockerfile');
|
||||
});
|
||||
|
||||
it('creates an apply resource command with a new resource using manifest', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
|
||||
cy.selectTab(TAB_COMMANDS);
|
||||
cy.getByDataCy('add').click();
|
||||
cy.getByDataCy('new-command-apply').click();
|
||||
cy.getByDataCy('command-apply-name').type('created-command');
|
||||
cy.getByDataCy('select-container').click().get('mat-option').contains('(New Resource)').click();
|
||||
cy.getByDataCy('resource-name').type('a-created-resource');
|
||||
cy.getByDataCy('resource-toggle-inlined').click();
|
||||
cy.getByDataCy('resource-manifest').type('spec: {}');
|
||||
cy.getByDataCy('resource-create').click();
|
||||
|
||||
cy.getByDataCy('select-container').should('contain', 'a-created-resource');
|
||||
cy.getByDataCy('command-apply-create').click();
|
||||
|
||||
cy.getByDataCy('command-info').first()
|
||||
.should('contain.text', 'created-command')
|
||||
.should('contain.text', 'a-created-resource');
|
||||
|
||||
cy.selectTab(TAB_RESOURCES);
|
||||
cy.getByDataCy('resource-info').first()
|
||||
.should('contain.text', 'a-created-resource')
|
||||
.should('contain.text', 'spec: {}');
|
||||
});
|
||||
|
||||
it('creates an apply resource command with a new resource using uri (default)', () => {
|
||||
cy.visit('http://localhost:4200');
|
||||
cy.clearDevfile();
|
||||
|
||||
cy.selectTab(TAB_COMMANDS);
|
||||
cy.getByDataCy('add').click();
|
||||
cy.getByDataCy('new-command-apply').click();
|
||||
cy.getByDataCy('command-apply-name').type('created-command');
|
||||
cy.getByDataCy('select-container').click().get('mat-option').contains('(New Resource)').click();
|
||||
cy.getByDataCy('resource-name').type('a-created-resource');
|
||||
cy.getByDataCy('resource-uri').type('/my/manifest.yaml');
|
||||
cy.getByDataCy('resource-create').click();
|
||||
|
||||
cy.getByDataCy('select-container').should('contain', 'a-created-resource');
|
||||
cy.getByDataCy('command-apply-create').click();
|
||||
|
||||
cy.getByDataCy('command-info').first()
|
||||
.should('contain.text', 'created-command')
|
||||
.should('contain.text', 'a-created-resource');
|
||||
|
||||
cy.selectTab(TAB_RESOURCES);
|
||||
cy.getByDataCy('resource-info').first()
|
||||
.should('contain.text', 'a-created-resource')
|
||||
.should('contain.text', 'URI')
|
||||
.should('contain.text', '/my/manifest.yaml');
|
||||
});
|
||||
});
|
||||
5
ui/cypress/fixtures/example.json
Normal file
5
ui/cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
10
ui/cypress/fixtures/input/with-apply-command.yaml
Normal file
10
ui/cypress/fixtures/input/with-apply-command.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
schemaVersion: 2.2.0
|
||||
metadata: {}
|
||||
commands:
|
||||
- apply:
|
||||
component: resource1
|
||||
id: command1
|
||||
components:
|
||||
- kubernetes:
|
||||
uri: uri
|
||||
name: resource1
|
||||
14
ui/cypress/fixtures/input/with-container.yaml
Normal file
14
ui/cypress/fixtures/input/with-container.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
schemaVersion: 2.2.0
|
||||
metadata: {}
|
||||
components:
|
||||
- container:
|
||||
args:
|
||||
- with
|
||||
- arg
|
||||
command:
|
||||
- the
|
||||
- command
|
||||
- to
|
||||
- run
|
||||
image: nginx
|
||||
name: container1
|
||||
23
ui/cypress/fixtures/input/with-exec-command.yaml
Normal file
23
ui/cypress/fixtures/input/with-exec-command.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
schemaVersion: 2.2.0
|
||||
metadata: {}
|
||||
commands:
|
||||
- exec:
|
||||
commandLine: ./build.sh
|
||||
component: container1
|
||||
hotReloadCapable: false
|
||||
workingDir: /projects
|
||||
id: command1
|
||||
components:
|
||||
- container:
|
||||
args:
|
||||
- with
|
||||
- arg
|
||||
command:
|
||||
- the
|
||||
- command
|
||||
- to
|
||||
- run
|
||||
dedicatedPod: false
|
||||
image: nginx
|
||||
mountSources: true
|
||||
name: container1
|
||||
14
ui/cypress/fixtures/input/with-image-command.yaml
Normal file
14
ui/cypress/fixtures/input/with-image-command.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
schemaVersion: 2.2.0
|
||||
metadata: {}
|
||||
commands:
|
||||
- apply:
|
||||
component: image1
|
||||
id: command1
|
||||
components:
|
||||
- image:
|
||||
autoBuild: false
|
||||
dockerfile:
|
||||
rootRequired: false
|
||||
uri: dockerfile
|
||||
imageName: nginx
|
||||
name: image1
|
||||
3
ui/cypress/fixtures/input/with-metadata-name.yaml
Normal file
3
ui/cypress/fixtures/input/with-metadata-name.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
schemaVersion: 2.2.0
|
||||
metadata:
|
||||
name: test-devfile
|
||||
64
ui/cypress/support/commands.ts
Normal file
64
ui/cypress/support/commands.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
Cypress.Commands.add('getByDataCy', (value: string) => {
|
||||
cy.get('[data-cy="'+value+'"]');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('selectTab', (n: number) => {
|
||||
cy.get('div[role=tab]').eq(n).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setDevfile', (devfile: string) => {
|
||||
cy.get('[data-cy="yaml-input"]').type(devfile);
|
||||
cy.get('[data-cy="yaml-save"]').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clearDevfile', () => {
|
||||
cy.get('[data-cy="yaml-clear"]', { timeout: 60000 }).click();
|
||||
});
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
getByDataCy(value: string): Chainable<void>
|
||||
selectTab(n: number): Chainable<void>
|
||||
|
||||
setDevfile(devfile: string): Chainable<void>
|
||||
clearDevfile(): Chainable<void>
|
||||
}
|
||||
}
|
||||
20
ui/cypress/support/e2e.ts
Normal file
20
ui/cypress/support/e2e.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/e2e.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
72
ui/devfile.yaml
Normal file
72
ui/devfile.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
commands:
|
||||
- composite:
|
||||
commands:
|
||||
- go-build
|
||||
- angular-install
|
||||
group:
|
||||
isDefault: true
|
||||
kind: build
|
||||
parallel: true
|
||||
id: build
|
||||
- exec:
|
||||
commandLine: npm run start
|
||||
component: node
|
||||
group:
|
||||
isDefault: true
|
||||
kind: run
|
||||
hotReloadCapable: true
|
||||
workingDir: ${PROJECT_SOURCE}
|
||||
id: run
|
||||
- exec:
|
||||
commandLine: make deploy
|
||||
component: node
|
||||
group:
|
||||
isDefault: true
|
||||
kind: deploy
|
||||
hotReloadCapable: false
|
||||
workingDir: ${PROJECTS_ROOT}
|
||||
id: deploy
|
||||
- exec:
|
||||
commandLine: make build-wasm
|
||||
component: go
|
||||
env:
|
||||
- name: GOPATH
|
||||
value: ${PROJECT_SOURCE}/.go
|
||||
- name: GOCACHE
|
||||
value: ${PROJECT_SOURCE}/.cache
|
||||
hotReloadCapable: false
|
||||
workingDir: ${PROJECT_SOURCE}
|
||||
id: go-build
|
||||
- exec:
|
||||
commandLine: npm install
|
||||
component: node
|
||||
hotReloadCapable: false
|
||||
workingDir: ${PROJECT_SOURCE}
|
||||
id: angular-install
|
||||
components:
|
||||
- container:
|
||||
args:
|
||||
- tail
|
||||
- -f
|
||||
- /dev/null
|
||||
dedicatedPod: false
|
||||
endpoints:
|
||||
- name: http-angular
|
||||
secure: false
|
||||
targetPort: 4200
|
||||
image: registry.access.redhat.com/ubi8/nodejs-16:latest
|
||||
memoryLimit: 4096Mi
|
||||
mountSources: true
|
||||
name: node
|
||||
- container:
|
||||
args:
|
||||
- tail
|
||||
- -f
|
||||
- /dev/null
|
||||
dedicatedPod: false
|
||||
image: registry.access.redhat.com/ubi9/go-toolset:1.18.9-14
|
||||
memoryLimit: 1024Mi
|
||||
mountSources: true
|
||||
name: go
|
||||
metadata: {}
|
||||
schemaVersion: 2.2.0
|
||||
14542
ui/package-lock.json
generated
Normal file
14542
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
ui/package.json
Normal file
48
ui/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "devfile-builder",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --host 0.0.0.0",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run",
|
||||
"deploy": "ng deploy --base-href='/'"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^15.2.0",
|
||||
"@angular/cdk": "^15.2.2",
|
||||
"@angular/common": "^15.2.0",
|
||||
"@angular/compiler": "^15.2.0",
|
||||
"@angular/core": "^15.2.0",
|
||||
"@angular/forms": "^15.2.0",
|
||||
"@angular/material": "^15.2.2",
|
||||
"@angular/platform-browser": "^15.2.0",
|
||||
"@angular/platform-browser-dynamic": "^15.2.0",
|
||||
"@angular/router": "^15.2.0",
|
||||
"mermaid": "^10.0.2",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^15.2.2",
|
||||
"@angular/cli": "~15.2.2",
|
||||
"@angular/compiler-cli": "^15.2.0",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/dompurify": "^2.4.0",
|
||||
"@types/jasmine": "~4.3.0",
|
||||
"angular-cli-ghpages": "^1.0.5",
|
||||
"cypress": "^12.11.0",
|
||||
"jasmine-core": "~4.5.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"typescript": "~4.9.4"
|
||||
}
|
||||
}
|
||||
1
ui/src/CNAME
Normal file
1
ui/src/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
devfile.odo.dev
|
||||
35
ui/src/app/app.component.css
Normal file
35
ui/src/app/app.component.css
Normal file
@@ -0,0 +1,35 @@
|
||||
main { min-height: calc(100vh - 100px); }
|
||||
|
||||
div.mermaid {
|
||||
/* font-family: 'trebuchet ms', verdana, arial; */
|
||||
font-family: 'Courier New', Courier, monospace !important;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-child {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex-child:first-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
#input {
|
||||
width: 99%;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
mat-form-field.full-width { width: 100%; }
|
||||
|
||||
div.tab-content { padding: 16px; }
|
||||
|
||||
div.error-message {
|
||||
font-size: large;
|
||||
margin: 16px;
|
||||
}
|
||||
90
ui/src/app/app.component.html
Normal file
90
ui/src/app/app.component.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<mat-toolbar color="primary">
|
||||
<span>Devfile Builder</span>
|
||||
<span class="spacer"></span>
|
||||
<span class="topright">Work in progress</span>
|
||||
<a mat-icon-button href="https://github.com/feloy/devfile-builder" target="_blank"><mat-icon svgIcon="github"></mat-icon></a>
|
||||
</mat-toolbar>
|
||||
<main>
|
||||
|
||||
<div class="flex-container">
|
||||
|
||||
<div class="flex-child">
|
||||
<mat-tab-group animationDuration="0">
|
||||
|
||||
<mat-tab data-cy="tab-yaml" label="YAML">
|
||||
<div class="tab-content">
|
||||
<div *ngIf="errorMessage" data-cy="yaml-error" class="error-message">{{errorMessage}}</div>
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Devfile YAML</mat-label>
|
||||
<textarea data-cy="yaml-input" matInput #input id="input" rows="20" [value]="devfileYaml"></textarea>
|
||||
</mat-form-field>
|
||||
<button data-cy="yaml-save" mat-flat-button color="primary" (click)="onButtonClick(input.value)">Save</button>
|
||||
<button data-cy="yaml-clear" mat-flat-button color="warn" (click)="clear()">Clear</button>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab data-cy="tab-chart" label="Chart">
|
||||
<div class="flex-child">
|
||||
<div #mermaid id="mermaid" class="mermaid" [innerHTML]="sanitizer.bypassSecurityTrustHtml(mermaidContent)"></div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab data-cy="tab-metadata">
|
||||
<ng-template mat-tab-label>
|
||||
Metadata
|
||||
</ng-template>
|
||||
<app-metadata></app-metadata>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon material-icons-outlined">code</mat-icon>
|
||||
Commands
|
||||
</ng-template>
|
||||
<app-commands></app-commands>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon material-icons-outlined">alarm</mat-icon>
|
||||
Events
|
||||
</ng-template>
|
||||
<app-events></app-events>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon material-icons-outlined">width_normal</mat-icon>
|
||||
Containers
|
||||
</ng-template>
|
||||
<app-containers></app-containers>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon material-icons-outlined">image</mat-icon>
|
||||
Images
|
||||
</ng-template>
|
||||
<app-images></app-images>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab >
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon material-icons-outlined">description</mat-icon>
|
||||
Resources
|
||||
</ng-template>
|
||||
<app-resources></app-resources>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon material-icons-outlined">storage</mat-icon>
|
||||
Volumes
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
31
ui/src/app/app.component.spec.ts
Normal file
31
ui/src/app/app.component.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'devfile-builder'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('devfile-builder');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('.content span')?.textContent).toContain('devfile-builder app is running!');
|
||||
});
|
||||
});
|
||||
74
ui/src/app/app.component.ts
Normal file
74
ui/src/app/app.component.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { WasmGoService } from './services/wasm-go.service';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { MermaidService } from './services/mermaid.service';
|
||||
import { StateService } from './services/state.service';
|
||||
import { MatIconRegistry } from "@angular/material/icon";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
|
||||
protected mermaidContent: string = "";
|
||||
protected devfileYaml: string = "";
|
||||
protected errorMessage: string = "";
|
||||
|
||||
constructor(
|
||||
protected sanitizer: DomSanitizer,
|
||||
private matIconRegistry: MatIconRegistry,
|
||||
private wasmGo: WasmGoService,
|
||||
private mermaid: MermaidService,
|
||||
private state: StateService,
|
||||
) {
|
||||
this.matIconRegistry.addSvgIcon(
|
||||
`github`,
|
||||
this.sanitizer.bypassSecurityTrustResourceUrl(`../assets/github-24.svg`)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const loading = document.getElementById("loading");
|
||||
if (loading != null) {
|
||||
loading.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
const devfile = this.state.getDevfile();
|
||||
if (devfile != null) {
|
||||
this.onButtonClick(devfile);
|
||||
}
|
||||
|
||||
this.state.state.subscribe(async newContent => {
|
||||
if (newContent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.devfileYaml = newContent.content;
|
||||
|
||||
try {
|
||||
const result = this.wasmGo.getFlowChart();
|
||||
const svg = await this.mermaid.getMermaidAsSVG(result);
|
||||
this.mermaidContent = svg;
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
|
||||
onButtonClick(content: string){
|
||||
const result = this.wasmGo.setDevfileContent(content);
|
||||
if (result.err != '') {
|
||||
this.errorMessage = result.err;
|
||||
} else {
|
||||
this.errorMessage = '';
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (confirm('You will delete the content of the Devfile. Continue?')) {
|
||||
this.state.resetDevfile();
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
114
ui/src/app/app.module.ts
Normal file
114
ui/src/app/app.module.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from "@angular/common/http";
|
||||
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { MetadataComponent } from './forms/metadata/metadata.component';
|
||||
import { MultiTextComponent } from './controls/multi-text/multi-text.component';
|
||||
import { ContainersComponent } from './tabs/containers/containers.component';
|
||||
import { ContainerComponent } from './forms/container/container.component';
|
||||
import { CommandsComponent } from './tabs/commands/commands.component';
|
||||
import { CommandExecComponent } from './forms/command-exec/command-exec.component';
|
||||
import { CommandApplyComponent } from './forms/command-apply/command-apply.component';
|
||||
import { CommandCompositeComponent } from './forms/command-composite/command-composite.component';
|
||||
import { SelectContainerComponent } from './controls/select-container/select-container.component';
|
||||
import { ResourcesComponent } from './tabs/resources/resources.component';
|
||||
import { ResourceComponent } from './forms/resource/resource.component';
|
||||
import { ImagesComponent } from './tabs/images/images.component';
|
||||
import { ImageComponent } from './forms/image/image.component';
|
||||
import { CommandImageComponent } from './forms/command-image/command-image.component';
|
||||
import { CommandsListComponent } from './lists/commands-list/commands-list.component';
|
||||
import { MultiCommandComponent } from './controls/multi-command/multi-command.component';
|
||||
import { EventsComponent } from './tabs/events/events.component';
|
||||
import { ChipsEventsComponent } from './controls/chips-events/chips-events.component';
|
||||
|
||||
declare const Go: any;
|
||||
|
||||
function loadWasmModule() {
|
||||
return () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const go = new Go();
|
||||
WebAssembly.instantiateStreaming(fetch("./assets/devfile.6ccdbea7789422ba86e8b08707f15f66.wasm"), go.importObject).then((result) => {
|
||||
go.run(result.instance);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
MetadataComponent,
|
||||
MultiTextComponent,
|
||||
ContainersComponent,
|
||||
ContainerComponent,
|
||||
CommandsComponent,
|
||||
CommandExecComponent,
|
||||
CommandApplyComponent,
|
||||
CommandCompositeComponent,
|
||||
SelectContainerComponent,
|
||||
ResourcesComponent,
|
||||
ResourceComponent,
|
||||
ImagesComponent,
|
||||
ImageComponent,
|
||||
CommandImageComponent,
|
||||
CommandsListComponent,
|
||||
MultiCommandComponent,
|
||||
EventsComponent,
|
||||
ChipsEventsComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
|
||||
DragDropModule,
|
||||
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule,
|
||||
MatTooltipModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: loadWasmModule,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -0,0 +1,3 @@
|
||||
.chip-list {
|
||||
width: 100%;
|
||||
}
|
||||
20
ui/src/app/controls/chips-events/chips-events.component.html
Normal file
20
ui/src/app/controls/chips-events/chips-events.component.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<mat-form-field class="chip-list" appearance="fill">
|
||||
<mat-label>Commands</mat-label>
|
||||
<mat-chip-grid #chipGrid>
|
||||
<mat-chip-row *ngFor="let cmd of commands" (removed)="remove(cmd)">
|
||||
{{cmd}}
|
||||
<button matChipRemove>
|
||||
<mat-icon class="material-icons-outlined">cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip-row>
|
||||
</mat-chip-grid>
|
||||
<input placeholder="New command..." #commandInput [formControl]="commandCtrl"
|
||||
[matChipInputFor]="chipGrid" [matAutocomplete]="auto"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
(matChipInputTokenEnd)="add($event)"/>
|
||||
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
|
||||
<mat-option *ngFor="let cmd of filteredCommands | async" [value]="cmd">
|
||||
{{cmd}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChipsEventsComponent } from './chips-events.component';
|
||||
|
||||
describe('ChipsEventsComponent', () => {
|
||||
let component: ChipsEventsComponent;
|
||||
let fixture: ComponentFixture<ChipsEventsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ChipsEventsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChipsEventsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
66
ui/src/app/controls/chips-events/chips-events.component.ts
Normal file
66
ui/src/app/controls/chips-events/chips-events.component.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import {COMMA, ENTER} from '@angular/cdk/keycodes';
|
||||
import { Observable, startWith, map } from 'rxjs';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chips-events',
|
||||
templateUrl: './chips-events.component.html',
|
||||
styleUrls: ['./chips-events.component.css']
|
||||
})
|
||||
export class ChipsEventsComponent {
|
||||
@Input() commands : string[] = [];
|
||||
@Input() allCommands: string[] = [];
|
||||
@Output() updated = new EventEmitter<string[]>();
|
||||
|
||||
separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
commandCtrl = new FormControl('');
|
||||
filteredCommands: Observable<string[]>;
|
||||
|
||||
|
||||
constructor(public commandInput :ElementRef<HTMLInputElement>) {
|
||||
this.filteredCommands = this.commandCtrl.valueChanges.pipe(
|
||||
startWith(null),
|
||||
map((cmd: string | null) => (cmd ? this._filter(cmd) : this.allCommands.slice())),
|
||||
);
|
||||
}
|
||||
|
||||
add(event: MatChipInputEvent): void {
|
||||
const value = (event.value || '').trim();
|
||||
|
||||
// Add our command
|
||||
if (value) {
|
||||
this.commands.push(value);
|
||||
this.updated.emit(this.commands);
|
||||
}
|
||||
|
||||
// Clear the input value
|
||||
event.chipInput!.clear();
|
||||
|
||||
this.commandCtrl.setValue(null);
|
||||
}
|
||||
|
||||
remove(command: string): void {
|
||||
const index = this.commands.indexOf(command);
|
||||
|
||||
if (index >= 0) {
|
||||
this.commands.splice(index, 1);
|
||||
this.updated.emit(this.commands);
|
||||
}
|
||||
}
|
||||
|
||||
selected(event: MatAutocompleteSelectedEvent): void {
|
||||
this.commands.push(event.option.viewValue);
|
||||
this.updated.emit(this.commands);
|
||||
this.commandInput.nativeElement.value = '';
|
||||
this.commandCtrl.setValue(null);
|
||||
}
|
||||
|
||||
private _filter(value: string): string[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
|
||||
return this.allCommands.filter(cmd => cmd.toLowerCase().includes(filterValue));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
h3 { margin-bottom: 0; }
|
||||
div.group { margin-bottom: 16px; }
|
||||
@@ -0,0 +1,14 @@
|
||||
<h3>{{title}}</h3>
|
||||
<div class="group">
|
||||
<span *ngFor="let command of commands; let i=index">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-select [value]="command" (selectionChange)="onCommandChange(i, $event.value)">
|
||||
<mat-option *ngFor="let commandElement of commandList" [value]="commandElement">{{commandElement}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</span>
|
||||
<button *ngIf="commands.length > 0" mat-icon-button (click)="addCommand()">
|
||||
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="commands.length == 0" mat-flat-button (click)="addCommand()">{{addLabel}}</button>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MultiCommandComponent } from './multi-command.component';
|
||||
|
||||
describe('MultiCommandComponent', () => {
|
||||
let component: MultiCommandComponent;
|
||||
let fixture: ComponentFixture<MultiCommandComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ MultiCommandComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MultiCommandComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
45
ui/src/app/controls/multi-command/multi-command.component.ts
Normal file
45
ui/src/app/controls/multi-command/multi-command.component.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-multi-command',
|
||||
templateUrl: './multi-command.component.html',
|
||||
styleUrls: ['./multi-command.component.css'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
multi: true,
|
||||
useExisting: MultiCommandComponent
|
||||
}
|
||||
]
|
||||
})
|
||||
export class MultiCommandComponent {
|
||||
|
||||
@Input() addLabel: string = "";
|
||||
@Input() commandList: string[] = [];
|
||||
@Input() title: string = "";
|
||||
|
||||
onChange = (_: string[]) => {};
|
||||
|
||||
commands: string[] = [];
|
||||
|
||||
writeValue(value: any) {
|
||||
this.commands = value;
|
||||
}
|
||||
|
||||
registerOnChange(onChange: any) {
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
registerOnTouched(_: any) {}
|
||||
|
||||
addCommand() {
|
||||
this.commands.push("");
|
||||
this.onChange(this.commands);
|
||||
}
|
||||
|
||||
onCommandChange(i: number, cmd: string) {
|
||||
this.commands[i] = cmd;
|
||||
this.onChange(this.commands);
|
||||
}
|
||||
}
|
||||
2
ui/src/app/controls/multi-text/multi-text.component.css
Normal file
2
ui/src/app/controls/multi-text/multi-text.component.css
Normal file
@@ -0,0 +1,2 @@
|
||||
h3 { margin-bottom: 0; }
|
||||
div.group { margin-bottom: 16px; }
|
||||
13
ui/src/app/controls/multi-text/multi-text.component.html
Normal file
13
ui/src/app/controls/multi-text/multi-text.component.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<h3>{{title}}</h3>
|
||||
<div class="group">
|
||||
<span *ngFor="let text of texts; let i=index">
|
||||
<mat-form-field class="inline" appearance="outline">
|
||||
<mat-label><span>{{label}}</span></mat-label>
|
||||
<input matInput [value]="text" (change)="onTextChange(i, $event)">
|
||||
</mat-form-field>
|
||||
</span>
|
||||
<button *ngIf="texts.length > 0" mat-icon-button (click)="addText()">
|
||||
<mat-icon class="tab-icon material-icons-outlined">add</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="texts.length == 0" mat-flat-button (click)="addText()">{{addLabel}}</button>
|
||||
</div>
|
||||
23
ui/src/app/controls/multi-text/multi-text.component.spec.ts
Normal file
23
ui/src/app/controls/multi-text/multi-text.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MultiTextComponent } from './multi-text.component';
|
||||
|
||||
describe('MultiTextComponent', () => {
|
||||
let component: MultiTextComponent;
|
||||
let fixture: ComponentFixture<MultiTextComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ MultiTextComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MultiTextComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
47
ui/src/app/controls/multi-text/multi-text.component.ts
Normal file
47
ui/src/app/controls/multi-text/multi-text.component.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-multi-text',
|
||||
templateUrl: './multi-text.component.html',
|
||||
styleUrls: ['./multi-text.component.css'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
multi: true,
|
||||
useExisting: MultiTextComponent
|
||||
}
|
||||
]
|
||||
|
||||
})
|
||||
export class MultiTextComponent implements ControlValueAccessor {
|
||||
|
||||
@Input() label: string = "";
|
||||
@Input() addLabel: string = "";
|
||||
@Input() title: string = "";
|
||||
|
||||
onChange = (_: string[]) => {};
|
||||
|
||||
texts: string[] = [];
|
||||
|
||||
writeValue(value: any) {
|
||||
this.texts = value;
|
||||
}
|
||||
|
||||
registerOnChange(onChange: any) {
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
registerOnTouched(_: any) {}
|
||||
|
||||
addText() {
|
||||
this.texts.push("");
|
||||
this.onChange(this.texts);
|
||||
}
|
||||
|
||||
onTextChange(i: number, e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
this.texts[i] = target.value;
|
||||
this.onChange(this.texts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>{{label}}</mat-label>
|
||||
<mat-select data-cy="select-container" [value]="container" (selectionChange)="onSelectChange($event.value)">
|
||||
<mat-option *ngFor="let container of containers" [value]="container">{{container}}</mat-option>
|
||||
<mat-option value="!">(New {{label}})</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SelectContainerComponent } from './select-container.component';
|
||||
|
||||
describe('SelectContainerComponent', () => {
|
||||
let component: SelectContainerComponent;
|
||||
let fixture: ComponentFixture<SelectContainerComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ SelectContainerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SelectContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-select-container',
|
||||
templateUrl: './select-container.component.html',
|
||||
styleUrls: ['./select-container.component.css'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
multi: true,
|
||||
useExisting: SelectContainerComponent
|
||||
}
|
||||
]
|
||||
})
|
||||
export class SelectContainerComponent implements ControlValueAccessor {
|
||||
|
||||
@Input() containers: string[] = [];
|
||||
@Input() label: string = "";
|
||||
@Output() createNew = new EventEmitter<boolean>();
|
||||
|
||||
container: string = "";
|
||||
|
||||
onChange = (_: string) => {};
|
||||
|
||||
writeValue(value: any) {
|
||||
this.container = value;
|
||||
}
|
||||
|
||||
registerOnChange(onChange: any) {
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
registerOnTouched(_: any) {}
|
||||
|
||||
onSelectChange(v: string) {
|
||||
if (v != "!") {
|
||||
this.onChange(v);
|
||||
}
|
||||
this.createNew.emit(v == "!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-form-field.full-width { width: 100%; }
|
||||
mat-form-field.mid-width { width: 50%; }
|
||||
24
ui/src/app/forms/command-apply/command-apply.component.html
Normal file
24
ui/src/app/forms/command-apply/command-apply.component.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="main">
|
||||
<h2>Add an Apply Command</h2>
|
||||
<div class="description">An Apply command "applies" a resource to the cluster. Equivalent to <code>kubectl apply -f ...</code></div>
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Name</span></mat-label>
|
||||
<mat-error>Lowercase words separated by dashes. Ex: my-command</mat-error>
|
||||
<input placeholder="unique name to identify the command" data-cy="command-apply-name" matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
<div><app-select-container
|
||||
formControlName="component"
|
||||
label="Resource"
|
||||
[containers]="resourceList"
|
||||
(createNew)="onCreateNewContainer($event)"></app-select-container></div>
|
||||
</form>
|
||||
|
||||
<app-resource
|
||||
*ngIf="showNewResource"
|
||||
(created)="onNewResourceCreated($event)"
|
||||
></app-resource>
|
||||
|
||||
<button data-cy="command-apply-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Apply Command" (click)="create()">Create</button>
|
||||
<button mat-flat-button (click)="cancel()">Cancel</button>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommandApplyComponent } from './command-apply.component';
|
||||
|
||||
describe('CommandApplyComponent', () => {
|
||||
let component: CommandApplyComponent;
|
||||
let fixture: ComponentFixture<CommandApplyComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CommandApplyComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CommandApplyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
70
ui/src/app/forms/command-apply/command-apply.component.ts
Normal file
70
ui/src/app/forms/command-apply/command-apply.component.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { ClusterResource, WasmGoService } from 'src/app/services/wasm-go.service';
|
||||
import { PATTERN_COMMAND_ID } from '../patterns';
|
||||
|
||||
@Component({
|
||||
selector: 'app-command-apply',
|
||||
templateUrl: './command-apply.component.html',
|
||||
styleUrls: ['./command-apply.component.css']
|
||||
})
|
||||
export class CommandApplyComponent {
|
||||
@Output() canceled = new EventEmitter<void>();
|
||||
|
||||
form: FormGroup;
|
||||
resourceList: string[] = [];
|
||||
showNewResource: boolean = false;
|
||||
resourceToCreate: ClusterResource | null = null;
|
||||
|
||||
constructor(
|
||||
private wasm: WasmGoService,
|
||||
private state: StateService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl("", [Validators.required, Validators.pattern(PATTERN_COMMAND_ID)]),
|
||||
component: new FormControl("", [Validators.required]),
|
||||
});
|
||||
|
||||
this.state.state.subscribe(async newContent => {
|
||||
const resources = newContent?.resources;
|
||||
if (resources == null) {
|
||||
return
|
||||
}
|
||||
this.resourceList = resources.map(resource => resource.name);
|
||||
});
|
||||
}
|
||||
|
||||
create() {
|
||||
if (this.resourceToCreate != null &&
|
||||
this.resourceToCreate?.name == this.form.controls["component"].value) {
|
||||
const result = this.wasm.addResource(this.resourceToCreate);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = this.wasm.addApplyCommand(this.form.value["name"], this.form.value);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.canceled.emit();
|
||||
}
|
||||
|
||||
onCreateNewContainer(v: boolean) {
|
||||
this.showNewResource = v;
|
||||
}
|
||||
|
||||
onNewResourceCreated(resource: ClusterResource) {
|
||||
this.resourceList.push(resource.name);
|
||||
this.form.controls["component"].setValue(resource.name);
|
||||
this.showNewResource = false;
|
||||
this.resourceToCreate = resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-form-field.full-width { width: 100%; }
|
||||
mat-form-field.mid-width { width: 50%; }
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="main">
|
||||
<h2>Add a Composite Command</h2>
|
||||
<div class="description">A Composite command executes several commands, either serially or in parallel.</div>
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Name</span></mat-label>
|
||||
<mat-error>Lowercase words separated by dashes. Ex: my-command</mat-error>
|
||||
<input placeholder="unique name to identify the command" data-cy="command-composite-name" matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
<div><mat-checkbox formControlName="parallel">Run commands in parallel</mat-checkbox></div>
|
||||
<app-multi-command formControlName="commands" title="Commands" addLabel="Add a command" [commandList]="commandList"></app-multi-command>
|
||||
</form>
|
||||
|
||||
<button data-cy="command-composite-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Composite Command" (click)="create()">Create</button>
|
||||
<button mat-flat-button (click)="cancel()">Cancel</button>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommandCompositeComponent } from './command-composite.component';
|
||||
|
||||
describe('CommandCompositeComponent', () => {
|
||||
let component: CommandCompositeComponent;
|
||||
let fixture: ComponentFixture<CommandCompositeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CommandCompositeComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CommandCompositeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { WasmGoService } from 'src/app/services/wasm-go.service';
|
||||
import { PATTERN_COMMAND_ID } from '../patterns';
|
||||
|
||||
@Component({
|
||||
selector: 'app-command-composite',
|
||||
templateUrl: './command-composite.component.html',
|
||||
styleUrls: ['./command-composite.component.css']
|
||||
})
|
||||
export class CommandCompositeComponent {
|
||||
@Output() canceled = new EventEmitter<void>();
|
||||
|
||||
form: FormGroup;
|
||||
commandList: string[] = [];
|
||||
|
||||
constructor(
|
||||
private wasm: WasmGoService,
|
||||
private state: StateService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl("", [Validators.required, Validators.pattern(PATTERN_COMMAND_ID)]),
|
||||
parallel: new FormControl(false),
|
||||
commands: new FormControl([])
|
||||
});
|
||||
|
||||
this.state.state.subscribe(async newContent => {
|
||||
const commands = newContent?.commands;
|
||||
if (commands == null) {
|
||||
return
|
||||
}
|
||||
this.commandList = commands.map(command => command.name);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
create() {
|
||||
const result = this.wasm.addCompositeCommand(this.form.value["name"], this.form.value);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.canceled.emit();
|
||||
}
|
||||
}
|
||||
3
ui/src/app/forms/command-exec/command-exec.component.css
Normal file
3
ui/src/app/forms/command-exec/command-exec.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-form-field.full-width { width: 100%; }
|
||||
mat-form-field.mid-width { width: 50%; }
|
||||
36
ui/src/app/forms/command-exec/command-exec.component.html
Normal file
36
ui/src/app/forms/command-exec/command-exec.component.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<div class="main">
|
||||
<h2>Add an Exec Command</h2>
|
||||
<div class="description">An Exec command is a shell command executed into a container.</div>
|
||||
<form [formGroup]="form">
|
||||
<div><mat-checkbox formControlName="hotReloadCapable">Hot Reload Capable</mat-checkbox></div>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Name</span></mat-label>
|
||||
<mat-error>Lowercase words separated by dashes. Ex: my-command</mat-error>
|
||||
<input placeholder="unique name to identify the command" data-cy="command-exec-name" matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Command Line</span></mat-label>
|
||||
<input placeholder="command line passed to the shell" data-cy="command-exec-command-line" matInput formControlName="commandLine">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Working Dir</span></mat-label>
|
||||
<input placeholder="Working directory of the command" data-cy="command-exec-working-dir" matInput formControlName="workingDir">
|
||||
</mat-form-field>
|
||||
<button data-cy="command-exec-projects-root" mat-button (click)="onProjectsRoot()">Work on Project's Root Directory</button>
|
||||
<div>
|
||||
<app-select-container
|
||||
formControlName="component"
|
||||
label="Container"
|
||||
[containers]="containerList"
|
||||
(createNew)="onCreateNewContainer($event)"></app-select-container>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<app-container
|
||||
*ngIf="showNewContainer"
|
||||
(created)="onNewContainerCreated($event)"
|
||||
></app-container>
|
||||
|
||||
<button data-cy="command-exec-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Exec Command" (click)="create()">Create</button>
|
||||
<button mat-flat-button (click)="cancel()">Cancel</button>
|
||||
</div>
|
||||
23
ui/src/app/forms/command-exec/command-exec.component.spec.ts
Normal file
23
ui/src/app/forms/command-exec/command-exec.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommandExecComponent } from './command-exec.component';
|
||||
|
||||
describe('CommandExecComponent', () => {
|
||||
let component: CommandExecComponent;
|
||||
let fixture: ComponentFixture<CommandExecComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CommandExecComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CommandExecComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
76
ui/src/app/forms/command-exec/command-exec.component.ts
Normal file
76
ui/src/app/forms/command-exec/command-exec.component.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Container, WasmGoService } from 'src/app/services/wasm-go.service';
|
||||
import { PATTERN_COMMAND_ID } from '../patterns';
|
||||
|
||||
@Component({
|
||||
selector: 'app-command-exec',
|
||||
templateUrl: './command-exec.component.html',
|
||||
styleUrls: ['./command-exec.component.css']
|
||||
})
|
||||
export class CommandExecComponent {
|
||||
@Output() canceled = new EventEmitter<void>();
|
||||
|
||||
form: FormGroup;
|
||||
containerList: string[] = [];
|
||||
showNewContainer: boolean = false;
|
||||
containerToCreate: Container | null = null;
|
||||
|
||||
constructor(
|
||||
private wasm: WasmGoService,
|
||||
private state: StateService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl("", [Validators.required, Validators.pattern(PATTERN_COMMAND_ID)]),
|
||||
component: new FormControl("", [Validators.required]),
|
||||
commandLine: new FormControl("", [Validators.required]),
|
||||
workingDir: new FormControl("", [Validators.required]),
|
||||
hotReloadCapable: new FormControl(false),
|
||||
});
|
||||
|
||||
this.state.state.subscribe(async newContent => {
|
||||
const containers = newContent?.containers;
|
||||
if (containers == null) {
|
||||
return
|
||||
}
|
||||
this.containerList = containers.map(container => container.name);
|
||||
});
|
||||
}
|
||||
|
||||
create() {
|
||||
if (this.containerToCreate != null &&
|
||||
this.containerToCreate?.name == this.form.controls["component"].value) {
|
||||
const result = this.wasm.addContainer(this.containerToCreate);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const result = this.wasm.addExecCommand(this.form.value["name"], this.form.value);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.canceled.emit();
|
||||
}
|
||||
|
||||
onProjectsRoot() {
|
||||
this.form.controls['workingDir'].setValue('${PROJECTS_ROOT}');
|
||||
}
|
||||
|
||||
onCreateNewContainer(v: boolean) {
|
||||
this.showNewContainer = v;
|
||||
}
|
||||
|
||||
onNewContainerCreated(container: Container) {
|
||||
this.containerList.push(container.name);
|
||||
this.form.controls["component"].setValue(container.name);
|
||||
this.showNewContainer = false;
|
||||
this.containerToCreate = container;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-form-field.full-width { width: 100%; }
|
||||
mat-form-field.mid-width { width: 50%; }
|
||||
24
ui/src/app/forms/command-image/command-image.component.html
Normal file
24
ui/src/app/forms/command-image/command-image.component.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="main">
|
||||
<h2>Add an Image Command</h2>
|
||||
<div class="description">An Image command builds a container image and pushes it to a container registry.</div>
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Name</span></mat-label>
|
||||
<mat-error>Lowercase words separated by dashes. Ex: my-command</mat-error>
|
||||
<input placeholder="unique name to identify the command" data-cy="command-image-name" matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
<div><app-select-container
|
||||
formControlName="component"
|
||||
label="Image"
|
||||
[containers]="imageList"
|
||||
(createNew)="onCreateNewImage($event)"></app-select-container></div>
|
||||
</form>
|
||||
|
||||
<app-image
|
||||
*ngIf="showNewImage"
|
||||
(created)="onNewImageCreated($event)"
|
||||
></app-image>
|
||||
|
||||
<button data-cy="command-image-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new Image Command" (click)="create()">Create</button>
|
||||
<button mat-flat-button (click)="cancel()">Cancel</button>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommandImageComponent } from './command-image.component';
|
||||
|
||||
describe('CommandImageComponent', () => {
|
||||
let component: CommandImageComponent;
|
||||
let fixture: ComponentFixture<CommandImageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CommandImageComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CommandImageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
70
ui/src/app/forms/command-image/command-image.component.ts
Normal file
70
ui/src/app/forms/command-image/command-image.component.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Image, WasmGoService } from 'src/app/services/wasm-go.service';
|
||||
import { PATTERN_COMMAND_ID } from '../patterns';
|
||||
|
||||
@Component({
|
||||
selector: 'app-command-image',
|
||||
templateUrl: './command-image.component.html',
|
||||
styleUrls: ['./command-image.component.css']
|
||||
})
|
||||
export class CommandImageComponent {
|
||||
@Output() canceled = new EventEmitter<void>();
|
||||
|
||||
form: FormGroup;
|
||||
imageList: string[] = [];
|
||||
showNewImage: boolean = false;
|
||||
imageToCreate: Image | null = null;
|
||||
|
||||
constructor(
|
||||
private wasm: WasmGoService,
|
||||
private state: StateService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl("", [Validators.required, Validators.pattern(PATTERN_COMMAND_ID)]),
|
||||
component: new FormControl("", [Validators.required]),
|
||||
});
|
||||
|
||||
this.state.state.subscribe(async newContent => {
|
||||
const images = newContent?.images;
|
||||
if (images == null) {
|
||||
return
|
||||
}
|
||||
this.imageList = images.map(image => image.name);
|
||||
});
|
||||
}
|
||||
|
||||
create() {
|
||||
if (this.imageToCreate != null &&
|
||||
this.imageToCreate?.name == this.form.controls["component"].value) {
|
||||
const result = this.wasm.addImage(this.imageToCreate);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = this.wasm.addApplyCommand(this.form.value["name"], this.form.value);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.canceled.emit();
|
||||
}
|
||||
|
||||
onCreateNewImage(v: boolean) {
|
||||
this.showNewImage = v;
|
||||
}
|
||||
|
||||
onNewImageCreated(image: Image) {
|
||||
this.imageList.push(image.name);
|
||||
this.form.controls["component"].setValue(image.name);
|
||||
this.showNewImage = false;
|
||||
this.imageToCreate = image;
|
||||
}
|
||||
}
|
||||
3
ui/src/app/forms/container/container.component.css
Normal file
3
ui/src/app/forms/container/container.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-form-field.full-width { width: 100%; }
|
||||
mat-form-field.mid-width { width: 50%; }
|
||||
40
ui/src/app/forms/container/container.component.html
Normal file
40
ui/src/app/forms/container/container.component.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<div class="main">
|
||||
<h2>Add a new container</h2>
|
||||
<div class="description">A Container is used to execute shell commands into a specific environment. The entrypoint of the container must be a non-terminating command. You can use an image pulled from a registry or an image built by an Image command.</div>
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Name</span></mat-label>
|
||||
<mat-error>Lowercase words separated by dashes. Ex: my-container</mat-error>
|
||||
<input placeholder="unique name to identify the container" data-cy="container-name" matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Image</span></mat-label>
|
||||
<input placeholder="Image to start the container" data-cy="container-image" matInput formControlName="image">
|
||||
</mat-form-field>
|
||||
<app-multi-text formControlName="command" title="Command" label="Command" addLabel="Add command"></app-multi-text>
|
||||
<app-multi-text formControlName="args" title="Arguments to command" label="Arg" addLabel="Add arg"></app-multi-text>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Memory Request</span></mat-label>
|
||||
<mat-error>{{quantityErrMsgMemory}}</mat-error>
|
||||
<input placeholder="memory requested for the container. Ex: 1Gi" data-cy="container-memory-request" matInput formControlName="memoryRequest">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Memory Limit</span></mat-label>
|
||||
<mat-error>{{quantityErrMsgMemory}}</mat-error>
|
||||
<input placeholder="memory limit for the container. Ex: 1Gi" data-cy="container-memory-limit" matInput formControlName="memoryLimit">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>CPU Request</span></mat-label>
|
||||
<mat-error>{{quantityErrMsgCPU}}</mat-error>
|
||||
<input placeholder="CPU requested for the container. Ex: 500m" data-cy="container-cpu-request" matInput formControlName="cpuRequest">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>CPU Limit</span></mat-label>
|
||||
<mat-error>{{quantityErrMsgCPU}}</mat-error>
|
||||
<input placeholder="CPU limit for the container. Ex: 1" data-cy="container-cpu-limit" matInput formControlName="cpuLimit">
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<button data-cy="container-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new container" (click)="create()">Create</button>
|
||||
<button *ngIf="cancelable" mat-flat-button (click)="cancel()">Cancel</button>
|
||||
</div>
|
||||
23
ui/src/app/forms/container/container.component.spec.ts
Normal file
23
ui/src/app/forms/container/container.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ContainerComponent } from './container.component';
|
||||
|
||||
describe('ContainerComponent', () => {
|
||||
let component: ContainerComponent;
|
||||
let fixture: ComponentFixture<ContainerComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ContainerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
59
ui/src/app/forms/container/container.component.ts
Normal file
59
ui/src/app/forms/container/container.component.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
|
||||
import { PATTERN_COMPONENT_ID } from '../patterns';
|
||||
import { Container, WasmGoService } from 'src/app/services/wasm-go.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-container',
|
||||
templateUrl: './container.component.html',
|
||||
styleUrls: ['./container.component.css']
|
||||
})
|
||||
export class ContainerComponent {
|
||||
@Input() cancelable: boolean = false;
|
||||
@Output() canceled = new EventEmitter<void>();
|
||||
@Output() created = new EventEmitter<Container>();
|
||||
|
||||
form: FormGroup;
|
||||
|
||||
quantityErrMsgMemory = 'Numeric value, with optional unit Ki, Mi, Gi, Ti, Pi, Ei';
|
||||
quantityErrMsgCPU = 'Numeric value, with optional unit m, k, M, G, T, P, E';
|
||||
|
||||
constructor(
|
||||
private wasm: WasmGoService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl("", [Validators.required, Validators.pattern(PATTERN_COMPONENT_ID)]),
|
||||
image: new FormControl("", [Validators.required]),
|
||||
command: new FormControl([]),
|
||||
args: new FormControl([]),
|
||||
memoryRequest: new FormControl("", [this.isQuantity()]),
|
||||
memoryLimit: new FormControl("", [this.isQuantity()]),
|
||||
cpuRequest: new FormControl("", [this.isQuantity()]),
|
||||
cpuLimit: new FormControl("", [this.isQuantity()]),
|
||||
})
|
||||
}
|
||||
|
||||
create() {
|
||||
this.created.emit(this.form.value);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.canceled.emit();
|
||||
}
|
||||
|
||||
isQuantity(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const val = control.value;
|
||||
if (val == '') {
|
||||
return null;
|
||||
}
|
||||
const valid = this.wasm.isQuantityValid(val);
|
||||
if (!valid) {
|
||||
return {
|
||||
"isQuantity": false,
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
3
ui/src/app/forms/image/image.component.css
Normal file
3
ui/src/app/forms/image/image.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-form-field.full-width { width: 100%; }
|
||||
mat-form-field.mid-width { width: 50%; }
|
||||
29
ui/src/app/forms/image/image.component.html
Normal file
29
ui/src/app/forms/image/image.component.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<div class="main">
|
||||
<h2>Add a new image</h2>
|
||||
<div class="description">An Image defines how to build a container image.</div>
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Name</span></mat-label>
|
||||
<mat-error>Lowercase words separated by dashes. Ex: my-image</mat-error>
|
||||
<input placeholder="unique name to identify the image" data-cy="image-name" matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Image Name</span></mat-label>
|
||||
<input placeholder="Reference to a container image" data-cy="image-image-name" matInput formControlName="imageName">
|
||||
</mat-form-field>
|
||||
<app-multi-text formControlName="args" title="Build Args" label="Arg" addLabel="Add Build Arg"></app-multi-text>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Build Context</span></mat-label>
|
||||
<input placeholder="Directory from which the build will be executed" data-cy="image-build-context" matInput formControlName="buildContext">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Dockerfile URI</span></mat-label>
|
||||
<input placeholder="Dockerfile used to build the image" data-cy="image-dockerfile-uri" matInput formControlName="uri">
|
||||
</mat-form-field>
|
||||
<mat-checkbox formControlName="rootRequired">Root Required</mat-checkbox>
|
||||
|
||||
</form>
|
||||
|
||||
<button data-cy="image-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new image" (click)="create()">Create</button>
|
||||
<button *ngIf="cancelable" mat-flat-button (click)="cancel()">Cancel</button>
|
||||
</div>
|
||||
23
ui/src/app/forms/image/image.component.spec.ts
Normal file
23
ui/src/app/forms/image/image.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ImageComponent } from './image.component';
|
||||
|
||||
describe('ImageComponent', () => {
|
||||
let component: ImageComponent;
|
||||
let fixture: ComponentFixture<ImageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ImageComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ImageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
36
ui/src/app/forms/image/image.component.ts
Normal file
36
ui/src/app/forms/image/image.component.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { Image } from 'src/app/services/wasm-go.service';
|
||||
import { PATTERN_COMPONENT_ID } from '../patterns';
|
||||
|
||||
@Component({
|
||||
selector: 'app-image',
|
||||
templateUrl: './image.component.html',
|
||||
styleUrls: ['./image.component.css']
|
||||
})
|
||||
export class ImageComponent {
|
||||
@Input() cancelable: boolean = false;
|
||||
@Output() canceled = new EventEmitter<void>();
|
||||
@Output() created = new EventEmitter<Image>();
|
||||
|
||||
form: FormGroup;
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl("", [Validators.required, Validators.pattern(PATTERN_COMPONENT_ID)]),
|
||||
imageName: new FormControl("", [Validators.required]),
|
||||
args: new FormControl([]),
|
||||
buildContext: new FormControl(""),
|
||||
rootRequired: new FormControl(false),
|
||||
uri: new FormControl("", [Validators.required]),
|
||||
})
|
||||
}
|
||||
|
||||
create() {
|
||||
this.created.emit(this.form.value);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.canceled.emit();
|
||||
}
|
||||
}
|
||||
3
ui/src/app/forms/metadata/metadata.component.css
Normal file
3
ui/src/app/forms/metadata/metadata.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-form-field.full-width { width: 100%; }
|
||||
mat-form-field.mid-width { width: 50%; }
|
||||
68
ui/src/app/forms/metadata/metadata.component.html
Normal file
68
ui/src/app/forms/metadata/metadata.component.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<div class="main">
|
||||
<form [formGroup]="form">
|
||||
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label>Name</mat-label>
|
||||
<input data-cy="metadata-name" placeholder="Unique name to identify the devfile" matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label>Version</mat-label>
|
||||
<mat-error>Examples: 1.0.4, 1.4.7-alpha1</mat-error>
|
||||
<input placeholder="Version of the devfile, semver-compatible" matInput formControlName="version">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Display Name</mat-label>
|
||||
<input placeholder="Name to display instead of the unique name" matInput formControlName="displayName">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Description</mat-label>
|
||||
<textarea matInput formControlName="description" rows="4"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label>Tags (comma-speparated)</mat-label>
|
||||
<input placeholder="Tags to help find the devfile in a registry" matInput formControlName="tags">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label>Architectures (comma-separated)</mat-label>
|
||||
<input placeholder="Ex: amd64,arm64,ppc64le,s390x" matInput formControlName="architectures">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label>Icon</mat-label>
|
||||
<input placeholder="Can be a URI or a relative path in the project" matInput formControlName="icon">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label>Global Memory Limit</mat-label>
|
||||
<input placeholder="Informative limit of memory used by the devfile. Ex: 1Gi" matInput formControlName="globalMemoryLimit">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label>Project Type</mat-label>
|
||||
<input placeholder="Ex: Framework of the project" matInput formControlName="projectType">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label>Language</mat-label>
|
||||
<input placeholder="Language of the project" matInput formControlName="language">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label>Website</mat-label>
|
||||
<input placeholder="Official website of the devfile" matInput formControlName="website">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label>Provider</mat-label>
|
||||
<input placeholder="Information about the provider of the devfile" matInput formControlName="provider">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Support URL</mat-label>
|
||||
<input placeholder="Link to a page providing support information" matInput formControlName="supportUrl">
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<button [disabled]="form.invalid" mat-flat-button color="primary" (click)="onSave()">Save</button>
|
||||
</div>
|
||||
23
ui/src/app/forms/metadata/metadata.component.spec.ts
Normal file
23
ui/src/app/forms/metadata/metadata.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MetadataComponent } from './metadata.component';
|
||||
|
||||
describe('MetadataComponent', () => {
|
||||
let component: MetadataComponent;
|
||||
let fixture: ComponentFixture<MetadataComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ MetadataComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MetadataComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
56
ui/src/app/forms/metadata/metadata.component.ts
Normal file
56
ui/src/app/forms/metadata/metadata.component.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { WasmGoService } from 'src/app/services/wasm-go.service';
|
||||
|
||||
const semverPattern = `^([0-9]+)\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$`;
|
||||
|
||||
@Component({
|
||||
selector: 'app-metadata',
|
||||
templateUrl: './metadata.component.html',
|
||||
styleUrls: ['./metadata.component.css']
|
||||
})
|
||||
export class MetadataComponent implements OnInit {
|
||||
|
||||
form: FormGroup;
|
||||
|
||||
constructor(
|
||||
private wasm: WasmGoService,
|
||||
private state: StateService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl(''),
|
||||
version: new FormControl('', Validators.pattern(semverPattern)),
|
||||
displayName: new FormControl(''),
|
||||
description: new FormControl(''),
|
||||
tags: new FormControl(""),
|
||||
architectures: new FormControl(""),
|
||||
icon: new FormControl(""),
|
||||
globalMemoryLimit: new FormControl(""),
|
||||
projectType: new FormControl(""),
|
||||
language: new FormControl(""),
|
||||
website: new FormControl(""),
|
||||
provider: new FormControl(""),
|
||||
supportUrl: new FormControl(""),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.state.state.subscribe(async newContent => {
|
||||
const metadata = newContent?.metadata;
|
||||
if (metadata == null) {
|
||||
return
|
||||
}
|
||||
this.form.setValue(metadata);
|
||||
});
|
||||
}
|
||||
|
||||
onSave() {
|
||||
const result = this.wasm.setMetadata(this.form.value);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
ui/src/app/forms/patterns.ts
Normal file
2
ui/src/app/forms/patterns.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const PATTERN_COMMAND_ID = "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$";
|
||||
export const PATTERN_COMPONENT_ID = "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$";
|
||||
4
ui/src/app/forms/resource/resource.component.css
Normal file
4
ui/src/app/forms/resource/resource.component.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.main { padding: 16px; }
|
||||
mat-form-field.full-width { width: 100%; }
|
||||
mat-form-field.mid-width { width: 50%; }
|
||||
span.toggleUriInlined { margin-left: 16px; }
|
||||
32
ui/src/app/forms/resource/resource.component.html
Normal file
32
ui/src/app/forms/resource/resource.component.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<div class="main">
|
||||
<h2>Add a new resource</h2>
|
||||
<div class="description">A Resource defines a Kubernetes resource. Its definition can be given either by a URI pointing to a manifest file or by an inlined YAML manifest.</div>
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field appearance="outline" class="mid-width">
|
||||
<mat-label><span>Name</span></mat-label>
|
||||
<mat-error>Lowercase words separated by dashes. Ex: my-resource</mat-error>
|
||||
<input placeholder="unique name to identify the resource" data-cy="resource-name" matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
|
||||
<span class="toggleUriInlined">
|
||||
<mat-button-toggle-group (change)="changeUriOrInlined($event.value)">
|
||||
<mat-button-toggle data-cy="resource-toogle-uri" value="uri" checked>Specify URI</mat-button-toggle>
|
||||
<mat-button-toggle data-cy="resource-toggle-inlined" value="inlined">Inlined content</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</span>
|
||||
|
||||
<mat-form-field *ngIf="uriOrInlined=='uri'" appearance="outline" class="full-width">
|
||||
<mat-label><span>URI</span></mat-label>
|
||||
<input placeholder="Reference to a YAML manifest" data-cy="resource-uri" matInput formControlName="uri">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field *ngIf="uriOrInlined=='inlined'" appearance="outline" class="full-width">
|
||||
<mat-label>YAML Manifest</mat-label>
|
||||
<textarea data-cy="resource-manifest" matInput formControlName="inlined" rows="8"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
</form>
|
||||
|
||||
<button data-cy="resource-create" [disabled]="form.invalid" mat-flat-button color="primary" matTooltip="create new resource" (click)="create()">Create</button>
|
||||
<button *ngIf="cancelable" mat-flat-button (click)="cancel()">Cancel</button>
|
||||
</div>
|
||||
23
ui/src/app/forms/resource/resource.component.spec.ts
Normal file
23
ui/src/app/forms/resource/resource.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ResourceComponent } from './resource.component';
|
||||
|
||||
describe('ResourceComponent', () => {
|
||||
let component: ResourceComponent;
|
||||
let fixture: ComponentFixture<ResourceComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ResourceComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ResourceComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
51
ui/src/app/forms/resource/resource.component.ts
Normal file
51
ui/src/app/forms/resource/resource.component.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ClusterResource } from 'src/app/services/wasm-go.service';
|
||||
import { PATTERN_COMPONENT_ID } from '../patterns';
|
||||
|
||||
@Component({
|
||||
selector: 'app-resource',
|
||||
templateUrl: './resource.component.html',
|
||||
styleUrls: ['./resource.component.css']
|
||||
})
|
||||
export class ResourceComponent {
|
||||
@Input() cancelable: boolean = false;
|
||||
@Output() canceled = new EventEmitter<void>();
|
||||
@Output() created = new EventEmitter<ClusterResource>();
|
||||
|
||||
form: FormGroup;
|
||||
uriOrInlined: string = 'uri';
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl("", [Validators.required, Validators.pattern(PATTERN_COMPONENT_ID)]),
|
||||
uri: new FormControl("", [Validators.required]),
|
||||
inlined: new FormControl("", []),
|
||||
})
|
||||
}
|
||||
|
||||
changeUriOrInlined(value: string) {
|
||||
this.uriOrInlined = value;
|
||||
if (this.uriOrInlined == 'uri') {
|
||||
this.form.controls['inlined'].removeValidators(Validators.required);
|
||||
this.form.controls['inlined'].setValue('');
|
||||
|
||||
this.form.controls['uri']?.addValidators(Validators.required);
|
||||
} else if (this.uriOrInlined == 'inlined') {
|
||||
this.form.controls['uri']?.removeValidators(Validators.required);
|
||||
this.form.controls['uri'].setValue('');
|
||||
|
||||
this.form.controls['inlined']?.setValidators(Validators.required);
|
||||
}
|
||||
this.form.controls['uri'].updateValueAndValidity()
|
||||
this.form.controls['inlined'].updateValueAndValidity()
|
||||
}
|
||||
|
||||
create() {
|
||||
this.created.emit(this.form.value);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.canceled.emit();
|
||||
}
|
||||
}
|
||||
29
ui/src/app/lists/commands-list/commands-list.component.css
Normal file
29
ui/src/app/lists/commands-list/commands-list.component.css
Normal file
@@ -0,0 +1,29 @@
|
||||
mat-card-header.with-right-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.space-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-card { margin-bottom: 16px; }
|
||||
mat-card-content { padding: 16px; }
|
||||
.command {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #eee;
|
||||
padding: 4px;
|
||||
margin: 4px;
|
||||
}
|
||||
.parallel-command {
|
||||
margin: 8px;
|
||||
}
|
||||
.serial-commands {
|
||||
margin: 4px;
|
||||
}
|
||||
div.nothing-here {
|
||||
margin: 0 4px 16px 4px;
|
||||
color: #00000054;
|
||||
}
|
||||
109
ui/src/app/lists/commands-list/commands-list.component.html
Normal file
109
ui/src/app/lists/commands-list/commands-list.component.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<div class="nothing-here" *ngIf="!getCommandsByKind(commands, kind)?.length && kind != ''">No {{kind}} commands yet. You can create a command then drag&drop it here</div>
|
||||
<div class="nothing-here" *ngIf="!getCommandsByKind(commands, kind)?.length && kind == ''">No generic commands yet. New commands will appear here</div>
|
||||
<ng-container *ngFor="let command of commands">
|
||||
<mat-card data-cy="command-info" cdkDrag [cdkDragDisabled]="dragDisabled" *ngIf="command.group == kind">
|
||||
<mat-card-header class="with-right-content colored-title">
|
||||
<div class="space-between">
|
||||
<mat-card-title>
|
||||
{{command.name}}
|
||||
</mat-card-title>
|
||||
<mat-checkbox
|
||||
*ngIf="command.group != ''"
|
||||
[checked]="command.default"
|
||||
(change)="toggleDefault($event, command.name, command.group)"
|
||||
>Default {{kind}} command</mat-checkbox>
|
||||
</div>
|
||||
<div>
|
||||
<mat-card-subtitle *ngIf="command.type == 'exec'">Exec Command</mat-card-subtitle>
|
||||
<mat-card-subtitle *ngIf="command.type == 'apply'">Apply Command</mat-card-subtitle>
|
||||
<mat-card-subtitle *ngIf="command.type == 'image'">Image Command</mat-card-subtitle>
|
||||
<mat-card-subtitle *ngIf="command.type == 'composite'">Composite Command</mat-card-subtitle>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
|
||||
|
||||
<ng-container *ngIf="command.type == 'exec'">
|
||||
<table class="aligned">
|
||||
<tr>
|
||||
<td>Is Hot Reload Capable:</td>
|
||||
<td>
|
||||
<span *ngIf="command.exec?.hotReloadCapable">Yes</span>
|
||||
<span *ngIf="!command.exec?.hotReloadCapable">No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Command Line:</td>
|
||||
<td><code>{{command.exec?.commandLine}}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Working Directory:</td>
|
||||
<td><code>{{command.exec?.workingDir}}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Container:</td>
|
||||
<td><mat-chip disableRipple>
|
||||
<mat-icon matChipAvatar class="material-icons-outlined">width_normal</mat-icon>
|
||||
{{command.exec?.component}}
|
||||
</mat-chip></td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="command.type == 'apply'">
|
||||
<table class="aligned">
|
||||
<tr>
|
||||
<td>Cluster resource:</td>
|
||||
<td><mat-chip disableRipple>
|
||||
<mat-icon matChipAvatar class="material-icons-outlined">description</mat-icon>
|
||||
{{command.apply?.component}}
|
||||
</mat-chip></td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="command.type == 'image'">
|
||||
<table class="aligned">
|
||||
<tr>
|
||||
<td>Image:</td>
|
||||
<td><mat-chip disableRipple>
|
||||
<mat-icon matChipAvatar class="material-icons-outlined">image</mat-icon>
|
||||
{{command.image?.component}}
|
||||
</mat-chip></td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="command.type == 'composite'">
|
||||
<table class="aligned">
|
||||
<tr>
|
||||
<td>Scheduling:</td>
|
||||
<td>
|
||||
<div *ngIf="command.composite?.parallel">Commands executed in parallel</div>
|
||||
<div *ngIf="!command.composite?.parallel">Commands executed serially</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commands:</td>
|
||||
<td>
|
||||
<mat-chip-set [class.mat-mdc-chip-set-stacked]="command.composite?.parallel">
|
||||
<mat-chip disableRipple *ngFor="let command of command.composite?.commands">
|
||||
<mat-icon matChipAvatar class="material-icons-outlined">code</mat-icon>
|
||||
{{command}}
|
||||
</mat-chip>
|
||||
</mat-chip-set>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions>
|
||||
<button mat-button color="warn" (click)="delete(command.name)">Delete</button>
|
||||
</mat-card-actions>
|
||||
|
||||
</mat-card>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommandsListComponent } from './commands-list.component';
|
||||
|
||||
describe('CommandsListComponent', () => {
|
||||
let component: CommandsListComponent;
|
||||
let fixture: ComponentFixture<CommandsListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CommandsListComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CommandsListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
62
ui/src/app/lists/commands-list/commands-list.component.ts
Normal file
62
ui/src/app/lists/commands-list/commands-list.component.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Component, EventEmitter, Input } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Command, WasmGoService } from 'src/app/services/wasm-go.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-commands-list',
|
||||
templateUrl: './commands-list.component.html',
|
||||
styleUrls: ['./commands-list.component.css']
|
||||
})
|
||||
export class CommandsListComponent {
|
||||
@Input() commands: Command[] | undefined;
|
||||
@Input() kind: string = "";
|
||||
@Input() dragDisabled: boolean = true;
|
||||
|
||||
constructor(
|
||||
private wasm: WasmGoService,
|
||||
private state: StateService,
|
||||
) {}
|
||||
|
||||
toggleDefault(event: MatCheckboxChange, command: string, group: string) {
|
||||
if (event.checked) {
|
||||
this.setDefault(command, group);
|
||||
} else {
|
||||
this.unsetDefault(command);
|
||||
}
|
||||
}
|
||||
|
||||
setDefault(command: string, group: string) {
|
||||
const result = this.wasm.setDefaultCommand(command, group);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
unsetDefault(command: string) {
|
||||
const result = this.wasm.unsetDefaultCommand(command);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
getCommandsByKind(commands: Command[] | undefined, kind: string ): Command[] | undefined {
|
||||
return commands?.filter((c: Command) => c.group == kind);
|
||||
}
|
||||
|
||||
delete(command: string) {
|
||||
if(confirm('You will delete the command "'+command+'". Continue?')) {
|
||||
const result = this.wasm.deleteCommand(command);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
ui/src/app/services/mermaid.service.spec.ts
Normal file
16
ui/src/app/services/mermaid.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MermaidService } from './mermaid.service';
|
||||
|
||||
describe('MermaidService', () => {
|
||||
let service: MermaidService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(MermaidService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
15
ui/src/app/services/mermaid.service.ts
Normal file
15
ui/src/app/services/mermaid.service.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import mermaid from 'mermaid';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MermaidService {
|
||||
|
||||
constructor() { }
|
||||
|
||||
async getMermaidAsSVG(definition: string): Promise<string> {
|
||||
const { svg } = await mermaid.render('rendered', definition);
|
||||
return svg;
|
||||
}
|
||||
}
|
||||
16
ui/src/app/services/state.service.spec.ts
Normal file
16
ui/src/app/services/state.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StateService } from './state.service';
|
||||
|
||||
describe('StateService', () => {
|
||||
let service: StateService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(StateService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
35
ui/src/app/services/state.service.ts
Normal file
35
ui/src/app/services/state.service.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { ResultValue } from './wasm-go.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StateService {
|
||||
|
||||
private _state = new BehaviorSubject<ResultValue | null>(null);
|
||||
public state = this._state.asObservable();
|
||||
|
||||
changeDevfileYaml(newValue: ResultValue) {
|
||||
localStorage.setItem("devfile", newValue.content);
|
||||
this._state.next(newValue);
|
||||
}
|
||||
|
||||
resetDevfile() {
|
||||
localStorage.removeItem('devfile');
|
||||
}
|
||||
|
||||
getDevfile(): string | null {
|
||||
return localStorage.getItem("devfile");
|
||||
}
|
||||
|
||||
getDragAndDropEnabled(): boolean {
|
||||
return localStorage.getItem("dragAndDropEnabled") == "true";
|
||||
}
|
||||
|
||||
saveDragAndDropEnabled(enabled: boolean) {
|
||||
return localStorage.setItem("dragAndDropEnabled", enabled ? "true" : "false");
|
||||
}
|
||||
}
|
||||
16
ui/src/app/services/wasm-go.service.spec.ts
Normal file
16
ui/src/app/services/wasm-go.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { WasmGoService } from './wasm-go.service';
|
||||
|
||||
describe('WasmGoService', () => {
|
||||
let service: WasmGoService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(WasmGoService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
241
ui/src/app/services/wasm-go.service.ts
Normal file
241
ui/src/app/services/wasm-go.service.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
type ChartResult = {
|
||||
err: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
type Result = {
|
||||
err: string;
|
||||
value: ResultValue;
|
||||
};
|
||||
|
||||
export type ResultValue = {
|
||||
content: string;
|
||||
metadata: Metadata;
|
||||
commands: Command[];
|
||||
events: Events;
|
||||
containers: Container[];
|
||||
images: Image[];
|
||||
resources: ClusterResource[];
|
||||
};
|
||||
|
||||
export type Metadata = {
|
||||
name: string | null;
|
||||
version: string | null;
|
||||
displayName: string | null;
|
||||
description: string | null;
|
||||
tags: string | null;
|
||||
architectures: string | null;
|
||||
icon: string | null;
|
||||
globalMemoryLimit: string | null;
|
||||
projectType: string | null;
|
||||
language: string | null;
|
||||
website: string | null;
|
||||
provider: string | null;
|
||||
supportUrl: string | null;
|
||||
};
|
||||
|
||||
export type Command = {
|
||||
name: string;
|
||||
group: string;
|
||||
default: boolean;
|
||||
type: "exec" | "apply" | "image" | "composite";
|
||||
exec: ExecCommand | undefined;
|
||||
apply: ApplyCommand | undefined;
|
||||
image: ImageCommand | undefined;
|
||||
composite: CompositeCommand | undefined;
|
||||
};
|
||||
|
||||
export type Events = {
|
||||
preStart: string[];
|
||||
postStart: string[];
|
||||
preStop: string[];
|
||||
postStop: string[];
|
||||
};
|
||||
|
||||
export type ExecCommand = {
|
||||
component: string;
|
||||
commandLine: string;
|
||||
workingDir: string;
|
||||
hotReloadCapable: boolean;
|
||||
};
|
||||
|
||||
export type ApplyCommand = {
|
||||
component: string;
|
||||
};
|
||||
|
||||
export type ImageCommand = {
|
||||
component: string;
|
||||
};
|
||||
|
||||
export type CompositeCommand = {
|
||||
commands: string[];
|
||||
parallel: boolean;
|
||||
};
|
||||
|
||||
export type Container = {
|
||||
name: string;
|
||||
image: string;
|
||||
command: string[];
|
||||
args: string[];
|
||||
memoryRequest: string;
|
||||
memoryLimit: string;
|
||||
cpuRequest: string;
|
||||
cpuLimit: string;
|
||||
};
|
||||
|
||||
export type Image = {
|
||||
name: string;
|
||||
imageName: string;
|
||||
args: string[];
|
||||
buildContext: string;
|
||||
rootRequired: boolean;
|
||||
uri: string;
|
||||
};
|
||||
|
||||
export type ClusterResource = {
|
||||
name: string;
|
||||
inlined: string;
|
||||
uri: string;
|
||||
};
|
||||
|
||||
declare const addContainer: (name: string, image: string, command: string[], args: string[], memReq: string, memLimit: string, cpuReq: string, cpuLimit: string) => Result;
|
||||
declare const addImage: (name: string, imageName: string, args: string[], buildContext: string, rootRequired: boolean, uri: string) => Result;
|
||||
declare const addResource: (name: string, inlined: string, uri: string) => Result;
|
||||
declare const addExecCommand: (name: string, component: string, commmandLine: string, workingDir: string, hotReloadCapable: boolean) => Result;
|
||||
declare const addApplyCommand: (name: string, component: string) => Result;
|
||||
declare const addCompositeCommand: (name: string, parallel: boolean, commands: string[]) => Result;
|
||||
declare const getFlowChart: () => ChartResult;
|
||||
declare const setDevfileContent: (devfile: string) => Result;
|
||||
declare const setMetadata: (metadata: Metadata) => Result;
|
||||
declare const moveCommand: (previousKind: string, newKind: string, previousIndex: number, newIndex: number) => Result;
|
||||
declare const setDefaultCommand: (command: string, group: string) => Result;
|
||||
declare const unsetDefaultCommand: (command: string) => Result;
|
||||
declare const deleteCommand: (command: string) => Result;
|
||||
declare const deleteContainer: (container: string) => Result;
|
||||
declare const deleteImage: (image: string) => Result;
|
||||
declare const deleteResource: (resource: string) => Result;
|
||||
declare const updateEvents: (event: string, commands: string[]) => Result;
|
||||
declare const isQuantityValid: (quantity: string) => Boolean;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
// WasmGoService uses the wasm module.
|
||||
// The module manages a single instance of a Devfile
|
||||
export class WasmGoService {
|
||||
|
||||
addContainer(container: Container): Result {
|
||||
return addContainer(
|
||||
container.name,
|
||||
container.image,
|
||||
container.command,
|
||||
container.args,
|
||||
container.memoryRequest,
|
||||
container.memoryLimit,
|
||||
container.cpuRequest,
|
||||
container.cpuLimit,
|
||||
);
|
||||
}
|
||||
|
||||
addImage(image: Image): Result {
|
||||
return addImage(
|
||||
image.name,
|
||||
image.imageName,
|
||||
image.args,
|
||||
image.buildContext,
|
||||
image.rootRequired,
|
||||
image.uri,
|
||||
);
|
||||
}
|
||||
|
||||
addResource(resource: ClusterResource): Result {
|
||||
return addResource(
|
||||
resource.name,
|
||||
resource.inlined,
|
||||
resource.uri,
|
||||
);
|
||||
}
|
||||
|
||||
addExecCommand(name: string, cmd: ExecCommand): Result {
|
||||
return addExecCommand(
|
||||
name,
|
||||
cmd.component,
|
||||
cmd.commandLine,
|
||||
cmd.workingDir,
|
||||
cmd.hotReloadCapable,
|
||||
);
|
||||
}
|
||||
|
||||
addApplyCommand(name: string, cmd: ApplyCommand): Result {
|
||||
return addApplyCommand(
|
||||
name,
|
||||
cmd.component,
|
||||
);
|
||||
}
|
||||
|
||||
addCompositeCommand(name: string, cmd: CompositeCommand): Result {
|
||||
return addCompositeCommand(
|
||||
name,
|
||||
cmd.parallel,
|
||||
cmd.commands,
|
||||
);
|
||||
}
|
||||
|
||||
// getFlowChart calls the wasm module to get the lifecycle of the Devfile in mermaid chart format
|
||||
getFlowChart(): string {
|
||||
const result = getFlowChart();
|
||||
return result.value;
|
||||
}
|
||||
|
||||
// setDevfileContent calls the wasm module to reset the content of the Devfile
|
||||
setDevfileContent(devfile: string): Result {
|
||||
const result = setDevfileContent(devfile);
|
||||
return result;
|
||||
}
|
||||
|
||||
setMetadata(metadata: Metadata): Result {
|
||||
return setMetadata(metadata);
|
||||
}
|
||||
|
||||
moveCommand(previousKind: string, newKind: string, previousIndex: number, newIndex: number): Result {
|
||||
return moveCommand(previousKind, newKind, previousIndex, newIndex);
|
||||
}
|
||||
|
||||
setDefaultCommand(command: string, group: string): Result {
|
||||
return setDefaultCommand(command, group);
|
||||
}
|
||||
|
||||
unsetDefaultCommand(command: string): Result {
|
||||
return unsetDefaultCommand(command);
|
||||
}
|
||||
|
||||
deleteCommand(command: string): Result {
|
||||
const result = deleteCommand(command);
|
||||
return result;
|
||||
}
|
||||
|
||||
deleteContainer(container: string): Result {
|
||||
const result = deleteContainer(container);
|
||||
return result;
|
||||
}
|
||||
|
||||
deleteImage(image: string): Result {
|
||||
const result = deleteImage(image);
|
||||
return result;
|
||||
}
|
||||
|
||||
deleteResource(resource: string): Result {
|
||||
const result = deleteResource(resource);
|
||||
return result;
|
||||
}
|
||||
|
||||
updateEvents(event: "preStart"|"postStart"|"preStop"|"postStop", commands: string[]): Result {
|
||||
return updateEvents(event, commands);
|
||||
}
|
||||
|
||||
isQuantityValid(quantity: string): Boolean {
|
||||
return isQuantityValid(quantity);
|
||||
}
|
||||
}
|
||||
22
ui/src/app/tabs/commands/commands.component.css
Normal file
22
ui/src/app/tabs/commands/commands.component.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.main { padding: 16px; }
|
||||
mat-card { margin-bottom: 16px; }
|
||||
mat-card-content { padding: 16px; }
|
||||
.command {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #eee;
|
||||
padding: 4px;
|
||||
margin: 4px;
|
||||
}
|
||||
.parallel-command {
|
||||
margin: 8px;
|
||||
}
|
||||
.serial-commands {
|
||||
margin: 4px;
|
||||
}
|
||||
h2 {
|
||||
color: #3f51b5;
|
||||
}
|
||||
div.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
80
ui/src/app/tabs/commands/commands.component.html
Normal file
80
ui/src/app/tabs/commands/commands.component.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<div class="main">
|
||||
<div class="align-right"><mat-checkbox [(ngModel)]="enableDragAndDrop" (ngModelChange)="enableDragAndDropChange()">Enable Drag and Drop</mat-checkbox></div>
|
||||
<div cdkDropListGroup>
|
||||
<div
|
||||
cdkDropList
|
||||
cdkDropListData="build"
|
||||
(cdkDropListDropped)="drop($event)">
|
||||
<h2>Build Commands</h2>
|
||||
<div class="description">When using odo, a Build command is the first command executed during the inner loop. The command is expected to terminate after the build is completed.</div>
|
||||
<app-commands-list kind="build" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
|
||||
</div>
|
||||
<div
|
||||
cdkDropList
|
||||
cdkDropListData="run"
|
||||
(cdkDropListDropped)="drop($event)">
|
||||
<h2>Run Commands</h2>
|
||||
<div class="description">When using odo, a Run command is executed during the inner loop after the Build command terminates. The command is expected to not terminate.</div>
|
||||
<app-commands-list kind="run" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
|
||||
</div>
|
||||
<div
|
||||
cdkDropList
|
||||
cdkDropListData="test"
|
||||
(cdkDropListDropped)="drop($event)">
|
||||
<h2>Test Commands</h2>
|
||||
<app-commands-list kind="test" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
|
||||
</div>
|
||||
<div
|
||||
cdkDropList
|
||||
cdkDropListData="debug"
|
||||
(cdkDropListDropped)="drop($event)">
|
||||
<h2>Debug Commands</h2>
|
||||
<div class="description">When using odo, a Debug command is executed during the inner loop after the Build command terminates. The command is expected to not terminate.</div>
|
||||
<app-commands-list kind="debug" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
|
||||
</div>
|
||||
<div
|
||||
cdkDropList
|
||||
cdkDropListData="deploy"
|
||||
(cdkDropListDropped)="drop($event)">
|
||||
<h2>Deploy Commands</h2>
|
||||
<div class="description">When using odo, a Deploy command is executed with <code>odo deploy</code>.</div>
|
||||
<app-commands-list kind="deploy" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
|
||||
</div>
|
||||
<div
|
||||
cdkDropList
|
||||
cdkDropListData=""
|
||||
(cdkDropListDropped)="drop($event)">
|
||||
<h2>Generic Commands</h2>
|
||||
<div class="description">Generic can be executed manually, or be part of composite commands and events.</div>
|
||||
<app-commands-list kind="" [dragDisabled]="!enableDragAndDrop" [commands]="commands"></app-commands-list>
|
||||
</div>
|
||||
</div>
|
||||
<app-command-exec (canceled)="undisplayExecForm()" *ngIf="forceDisplayExecForm"></app-command-exec>
|
||||
<app-command-apply (canceled)="undisplayApplyForm()" *ngIf="forceDisplayApplyForm"></app-command-apply>
|
||||
<app-command-image (canceled)="undisplayImageForm()" *ngIf="forceDisplayImageForm"></app-command-image>
|
||||
<app-command-composite (canceled)="undisplayCompositeForm()" *ngIf="forceDisplayCompositeForm"></app-command-composite>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!forceDisplayExecForm && !forceDisplayApplyForm && !forceDisplayImageForm && !forceDisplayCompositeForm">
|
||||
<button data-cy="add" class="fab" mat-fab color="primary" [matMenuTriggerFor]="menu">
|
||||
<mat-icon class="material-icons-outlined">add</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<mat-menu #menu="matMenu" yPosition="above" xPosition="before">
|
||||
<button data-cy="new-command-exec" mat-menu-item (click)="displayExecForm()">
|
||||
<mat-icon class="tab-icon material-icons-outlined">width_normal</mat-icon>
|
||||
<span>Exec command</span>
|
||||
</button>
|
||||
<button data-cy="new-command-image" mat-menu-item (click)="displayImageForm()">
|
||||
<mat-icon class="tab-icon material-icons-outlined">image</mat-icon>
|
||||
<span>Image command</span>
|
||||
</button>
|
||||
<button data-cy="new-command-apply" mat-menu-item (click)="displayApplyForm()">
|
||||
<mat-icon class="tab-icon material-icons-outlined">description</mat-icon>
|
||||
<span>Apply command</span>
|
||||
</button>
|
||||
<button data-cy="new-command-composite" mat-menu-item (click)="displayCompositeForm()">
|
||||
<span>Composite command</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
23
ui/src/app/tabs/commands/commands.component.spec.ts
Normal file
23
ui/src/app/tabs/commands/commands.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommandsComponent } from './commands.component';
|
||||
|
||||
describe('CommandsComponent', () => {
|
||||
let component: CommandsComponent;
|
||||
let fixture: ComponentFixture<CommandsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CommandsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CommandsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
110
ui/src/app/tabs/commands/commands.component.ts
Normal file
110
ui/src/app/tabs/commands/commands.component.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||
import { Component } from '@angular/core';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Command, WasmGoService } from 'src/app/services/wasm-go.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-commands',
|
||||
templateUrl: './commands.component.html',
|
||||
styleUrls: ['./commands.component.css']
|
||||
})
|
||||
export class CommandsComponent {
|
||||
forceDisplayExecForm: boolean = false;
|
||||
forceDisplayApplyForm: boolean = false;
|
||||
forceDisplayImageForm: boolean = false;
|
||||
forceDisplayCompositeForm: boolean = false;
|
||||
enableDragAndDrop: boolean;
|
||||
|
||||
commands: Command[] | undefined = [];
|
||||
|
||||
constructor(
|
||||
private state: StateService,
|
||||
private wasm: WasmGoService,
|
||||
) {
|
||||
this.enableDragAndDrop = this.state.getDragAndDropEnabled();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.state.state.subscribe(async newContent => {
|
||||
this.commands = newContent?.commands;
|
||||
if (this.commands == null) {
|
||||
return
|
||||
}
|
||||
this.forceDisplayExecForm = false;
|
||||
this.forceDisplayApplyForm = false;
|
||||
this.forceDisplayImageForm = false;
|
||||
this.forceDisplayCompositeForm = false;
|
||||
});
|
||||
}
|
||||
|
||||
displayExecForm() {
|
||||
this.forceDisplayExecForm = true;
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
displayApplyForm() {
|
||||
this.forceDisplayApplyForm = true;
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
displayImageForm() {
|
||||
this.forceDisplayImageForm = true;
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
displayCompositeForm() {
|
||||
this.forceDisplayCompositeForm = true;
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
undisplayExecForm() {
|
||||
this.forceDisplayExecForm = false;
|
||||
}
|
||||
|
||||
undisplayApplyForm() {
|
||||
this.forceDisplayApplyForm = false;
|
||||
}
|
||||
|
||||
undisplayImageForm() {
|
||||
this.forceDisplayImageForm = false;
|
||||
}
|
||||
|
||||
undisplayCompositeForm() {
|
||||
this.forceDisplayCompositeForm = false;
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string>) {
|
||||
this.moveCommand(
|
||||
event.previousContainer.data,
|
||||
event.container.data,
|
||||
event.previousIndex,
|
||||
event.currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
moveCommand(previousKind: string, newKind: string, previousIndex: number, newIndex: number) {
|
||||
const result = this.wasm.moveCommand(previousKind, newKind, previousIndex, newIndex);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
enableDragAndDropChange() {
|
||||
this.state.saveDragAndDropEnabled(this.enableDragAndDrop);
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
window.scrollTo(0,document.body.scrollHeight);
|
||||
}
|
||||
|
||||
}
|
||||
3
ui/src/app/tabs/containers/containers.component.css
Normal file
3
ui/src/app/tabs/containers/containers.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-card { margin-bottom: 16px; }
|
||||
mat-card-content { padding: 16px; }
|
||||
60
ui/src/app/tabs/containers/containers.component.html
Normal file
60
ui/src/app/tabs/containers/containers.component.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<div class="main">
|
||||
<mat-card data-cy="container-info" *ngFor="let container of containers">
|
||||
<mat-card-header class="colored-title">
|
||||
<mat-card-title>{{container.name}}</mat-card-title>
|
||||
<mat-card-subtitle>Container</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<table class="aligned">
|
||||
<tr>
|
||||
<td>Image:</td>
|
||||
<td><code>{{container.image}}</code></td>
|
||||
</tr>
|
||||
<tr *ngIf="container.command.length > 0">
|
||||
<td>Command:</td>
|
||||
<td><code>{{container.command.join(" ")}}</code></td>
|
||||
</tr>
|
||||
<tr *ngIf="container.args.length > 0">
|
||||
<td>Args:</td>
|
||||
<td><code>{{container.args.join(" ")}}</code></td>
|
||||
</tr>
|
||||
<tr *ngIf="container.memoryRequest.length > 0">
|
||||
<td>Memory Request:</td>
|
||||
<td><code>{{container.memoryRequest}}</code></td>
|
||||
</tr>
|
||||
<tr *ngIf="container.memoryLimit.length > 0">
|
||||
<td>Memory Limit:</td>
|
||||
<td><code>{{container.memoryLimit}}</code></td>
|
||||
</tr>
|
||||
<tr *ngIf="container.cpuRequest.length > 0">
|
||||
<td>CPU Request:</td>
|
||||
<td><code>{{container.cpuRequest}}</code></td>
|
||||
</tr>
|
||||
<tr *ngIf="container.cpuLimit.length > 0">
|
||||
<td>CPU Limit:</td>
|
||||
<td><code>{{container.cpuLimit}}</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions>
|
||||
<button mat-button color="warn" (click)="delete(container.name)">Delete</button>
|
||||
</mat-card-actions>
|
||||
|
||||
</mat-card>
|
||||
|
||||
<app-container
|
||||
*ngIf="forceDisplayAdd || containers == undefined || containers.length == 0"
|
||||
[cancelable]="forceDisplayAdd"
|
||||
(canceled)="undisplayAddForm()"
|
||||
(created)="onCreated($event)"
|
||||
></app-container>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-container *ngIf="!forceDisplayAdd && containers != undefined && containers.length > 0">
|
||||
<button data-cy="add" class="fab" mat-fab color="primary" (click)="displayAddForm()">
|
||||
<mat-icon class="material-icons-outlined">add</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
23
ui/src/app/tabs/containers/containers.component.spec.ts
Normal file
23
ui/src/app/tabs/containers/containers.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ContainersComponent } from './containers.component';
|
||||
|
||||
describe('ContainersComponent', () => {
|
||||
let component: ContainersComponent;
|
||||
let fixture: ComponentFixture<ContainersComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ContainersComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ContainersComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
65
ui/src/app/tabs/containers/containers.component.ts
Normal file
65
ui/src/app/tabs/containers/containers.component.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Container, WasmGoService } from 'src/app/services/wasm-go.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-containers',
|
||||
templateUrl: './containers.component.html',
|
||||
styleUrls: ['./containers.component.css']
|
||||
})
|
||||
export class ContainersComponent implements OnInit {
|
||||
|
||||
forceDisplayAdd: boolean = false;
|
||||
containers: Container[] | undefined = [];
|
||||
|
||||
constructor(
|
||||
private state: StateService,
|
||||
private wasm: WasmGoService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
const that = this;
|
||||
this.state.state.subscribe(async newContent => {
|
||||
that.containers = newContent?.containers;
|
||||
if (this.containers == null) {
|
||||
return
|
||||
}
|
||||
that.forceDisplayAdd = false;
|
||||
});
|
||||
}
|
||||
|
||||
displayAddForm() {
|
||||
this.forceDisplayAdd = true;
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
undisplayAddForm() {
|
||||
this.forceDisplayAdd = false;
|
||||
}
|
||||
|
||||
delete(name: string) {
|
||||
if(confirm('You will delete the container "'+name+'". Continue?')) {
|
||||
const result = this.wasm.deleteContainer(name);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCreated(container: Container) {
|
||||
const result = this.wasm.addContainer(container);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
window.scrollTo(0,document.body.scrollHeight);
|
||||
}
|
||||
}
|
||||
4
ui/src/app/tabs/events/events.component.css
Normal file
4
ui/src/app/tabs/events/events.component.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.main { padding: 16px; }
|
||||
h2 {
|
||||
color: #3f51b5;
|
||||
}
|
||||
34
ui/src/app/tabs/events/events.component.html
Normal file
34
ui/src/app/tabs/events/events.component.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<div class="main">
|
||||
<h2>Pre-Start event</h2>
|
||||
<div class="description">Pre-Start commands are executed before the inner loop is started, inside init-containers (not implemented by odo).</div>
|
||||
<app-chips-events
|
||||
[commands]="events?.preStart ?? []"
|
||||
[allCommands]="allCommands ?? []"
|
||||
(updated)="onUpdate('preStart', $event)"
|
||||
></app-chips-events>
|
||||
|
||||
<h2>Post-Start event</h2>
|
||||
<div class="description">Post-Start commands are executed at the beginning of the inner loop, inside pre-fetched containers.</div>
|
||||
<app-chips-events
|
||||
[commands]="events?.postStart ?? []"
|
||||
[allCommands]="allCommands ?? []"
|
||||
(updated)="onUpdate('postStart', $event)"
|
||||
></app-chips-events>
|
||||
|
||||
<h2>Pre-Stop event</h2>
|
||||
<div class="description">Pre-Stop commands are executed at the end of the inner loop, inside pre-fetched containers.</div>
|
||||
<app-chips-events
|
||||
[commands]="events?.preStop ?? []"
|
||||
[allCommands]="allCommands ?? []"
|
||||
(updated)="onUpdate('preStop', $event)"
|
||||
></app-chips-events>
|
||||
|
||||
<h2>Post-Stop event</h2>
|
||||
<div class="description">Post-Stop commands are executed after the inner loop is finished (not implemented by odo).</div>
|
||||
<app-chips-events
|
||||
[commands]="events?.postStop ?? []"
|
||||
[allCommands]="allCommands ?? []"
|
||||
(updated)="onUpdate('postStop', $event)"
|
||||
></app-chips-events>
|
||||
|
||||
</div>
|
||||
23
ui/src/app/tabs/events/events.component.spec.ts
Normal file
23
ui/src/app/tabs/events/events.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EventsComponent } from './events.component';
|
||||
|
||||
describe('EventsComponent', () => {
|
||||
let component: EventsComponent;
|
||||
let fixture: ComponentFixture<EventsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ EventsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EventsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
35
ui/src/app/tabs/events/events.component.ts
Normal file
35
ui/src/app/tabs/events/events.component.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Events, WasmGoService } from 'src/app/services/wasm-go.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-events',
|
||||
templateUrl: './events.component.html',
|
||||
styleUrls: ['./events.component.css']
|
||||
})
|
||||
export class EventsComponent {
|
||||
|
||||
events: Events | undefined;
|
||||
allCommands: string[] | undefined;
|
||||
|
||||
constructor(
|
||||
private state: StateService,
|
||||
private wasm: WasmGoService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.state.state.subscribe(async newContent => {
|
||||
this.events = newContent?.events;
|
||||
this.allCommands = newContent?.commands.map(c => c.name);
|
||||
});
|
||||
}
|
||||
|
||||
onUpdate(event: "preStart" | "postStart" | "preStop" | "postStop", commands: string[]) {
|
||||
const result = this.wasm.updateEvents(event, commands);
|
||||
if (result.err != '') {
|
||||
alert(result.err);
|
||||
} else {
|
||||
this.state.changeDevfileYaml(result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
ui/src/app/tabs/images/images.component.css
Normal file
3
ui/src/app/tabs/images/images.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.main { padding: 16px; }
|
||||
mat-card { margin-bottom: 16px; }
|
||||
mat-card-content { padding: 16px; }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user