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:
Philippe Martin
2023-06-29 11:06:02 +02:00
committed by GitHub
parent ce1d824886
commit 6a4e964d5e
3215 changed files with 1088400 additions and 2 deletions

46
ui/.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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');
});
});

View 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"
}

View File

@@ -0,0 +1,10 @@
schemaVersion: 2.2.0
metadata: {}
commands:
- apply:
component: resource1
id: command1
components:
- kubernetes:
uri: uri
name: resource1

View File

@@ -0,0 +1,14 @@
schemaVersion: 2.2.0
metadata: {}
components:
- container:
args:
- with
- arg
command:
- the
- command
- to
- run
image: nginx
name: container1

View 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

View 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

View File

@@ -0,0 +1,3 @@
schemaVersion: 2.2.0
metadata:
name: test-devfile

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

48
ui/package.json Normal file
View 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
View File

@@ -0,0 +1 @@
devfile.odo.dev

View 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;
}

View 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>

View 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!');
});
});

View 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
View 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 { }

View File

@@ -0,0 +1,3 @@
.chip-list {
width: 100%;
}

View 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>

View File

@@ -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();
});
});

View 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));
}
}

View File

@@ -0,0 +1,2 @@
h3 { margin-bottom: 0; }
div.group { margin-bottom: 16px; }

View File

@@ -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>

View File

@@ -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();
});
});

View 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);
}
}

View File

@@ -0,0 +1,2 @@
h3 { margin-bottom: 0; }
div.group { margin-bottom: 16px; }

View 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>

View 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();
});
});

View 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);
}
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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 == "!");
}
}

View File

@@ -0,0 +1,3 @@
.main { padding: 16px; }
mat-form-field.full-width { width: 100%; }
mat-form-field.mid-width { width: 50%; }

View 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>

View File

@@ -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();
});
});

View 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;
}
}

View File

@@ -0,0 +1,3 @@
.main { padding: 16px; }
mat-form-field.full-width { width: 100%; }
mat-form-field.mid-width { width: 50%; }

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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();
}
}

View File

@@ -0,0 +1,3 @@
.main { padding: 16px; }
mat-form-field.full-width { width: 100%; }
mat-form-field.mid-width { width: 50%; }

View 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>

View 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();
});
});

View 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;
}
}

View File

@@ -0,0 +1,3 @@
.main { padding: 16px; }
mat-form-field.full-width { width: 100%; }
mat-form-field.mid-width { width: 50%; }

View 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>

View File

@@ -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();
});
});

View 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;
}
}

View File

@@ -0,0 +1,3 @@
.main { padding: 16px; }
mat-form-field.full-width { width: 100%; }
mat-form-field.mid-width { width: 50%; }

View 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>

View 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();
});
});

View 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;
};
}
}

View File

@@ -0,0 +1,3 @@
.main { padding: 16px; }
mat-form-field.full-width { width: 100%; }
mat-form-field.mid-width { width: 50%; }

View 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>

View 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();
});
});

View 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();
}
}

View File

@@ -0,0 +1,3 @@
.main { padding: 16px; }
mat-form-field.full-width { width: 100%; }
mat-form-field.mid-width { width: 50%; }

View 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>

View 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();
});
});

View 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);
}
}
}

View 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])?$";

View 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; }

View 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>

View 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();
});
});

View 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();
}
}

View 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;
}

View 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>

View File

@@ -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();
});
});

View 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);
}
}
}
}

View 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();
});
});

View 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;
}
}

View 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();
});
});

View 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");
}
}

View 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();
});
});

View 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);
}
}

View 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;
}

View 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>

View 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();
});
});

View 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);
}
}

View File

@@ -0,0 +1,3 @@
.main { padding: 16px; }
mat-card { margin-bottom: 16px; }
mat-card-content { padding: 16px; }

View 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>

View 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();
});
});

View 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);
}
}

View File

@@ -0,0 +1,4 @@
.main { padding: 16px; }
h2 {
color: #3f51b5;
}

View 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>

View 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();
});
});

View 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);
}
}
}

View File

@@ -0,0 +1,3 @@
.main { padding: 16px; }
mat-card { margin-bottom: 16px; }
mat-card-content { padding: 16px; }

View File

@@ -0,0 +1,50 @@
<div class="main">
<mat-card data-cy="image-info" *ngFor="let image of images">
<mat-card-header class="colored-title">
<mat-card-title>{{image.name}}</mat-card-title>
<mat-card-subtitle>Image</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<table class="aligned">
<tr>
<td>Image Name:</td>
<td><code>{{image.imageName}}</code></td>
</tr>
<tr>
<td>Dockerfile URI:</td>
<td><code>{{image.uri}}</code></td>
</tr>
<tr *ngIf="image.args.length > 0">
<td>Build Args:</td>
<td><code>{{image.args}}</code></td>
</tr>
<tr>
<td>Build Context:</td>
<td><code>{{image.buildContext}}</code></td>
</tr>
<tr>
<td>Root Required:</td>
<td><code>{{image.rootRequired ? "Yes" : "No"}}</code></td>
</tr>
</table>
</mat-card-content>
<mat-card-actions>
<button mat-button color="warn" (click)="delete(image.name)">Delete</button>
</mat-card-actions>
</mat-card>
<app-image
*ngIf="forceDisplayAdd || images == undefined || images.length == 0"
[cancelable]="forceDisplayAdd"
(canceled)="undisplayAddForm()"
(created)="onCreated($event)"
></app-image>
</div>
<ng-container *ngIf="!forceDisplayAdd && images != undefined && images.length > 0">
<button class="fab" mat-fab color="primary" (click)="displayAddForm()">
<mat-icon class="material-icons-outlined">add</mat-icon>
</button>
</ng-container>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ImagesComponent } from './images.component';
describe('ImagesComponent', () => {
let component: ImagesComponent;
let fixture: ComponentFixture<ImagesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ImagesComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(ImagesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,65 @@
import { Component, OnInit } from '@angular/core';
import { StateService } from 'src/app/services/state.service';
import { Image, WasmGoService } from 'src/app/services/wasm-go.service';
@Component({
selector: 'app-images',
templateUrl: './images.component.html',
styleUrls: ['./images.component.css']
})
export class ImagesComponent implements OnInit {
forceDisplayAdd: boolean = false;
images: Image[] | undefined = [];
constructor(
private state: StateService,
private wasm: WasmGoService,
) {}
ngOnInit() {
const that = this;
this.state.state.subscribe(async newContent => {
that.images = newContent?.images;
if (this.images == 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 image "'+name+'". Continue?')) {
const result = this.wasm.deleteImage(name);
if (result.err != '') {
alert(result.err);
} else {
this.state.changeDevfileYaml(result.value);
}
}
}
onCreated(image: Image) {
const result = this.wasm.addImage(image);
if (result.err != '') {
alert(result.err);
} else {
this.state.changeDevfileYaml(result.value);
}
}
scrollToBottom() {
window.scrollTo(0,document.body.scrollHeight);
}
}

Some files were not shown because too many files have changed in this diff Show More