├── .github ├── dco.yml ├── labels-manage.yml ├── settings.xml └── workflows │ ├── build-snapshot-worker.yml │ ├── ci-e2e.yml │ ├── ci-pr.yml │ ├── ci.yml │ ├── issue-handler.yml │ ├── label-manage.yml │ ├── milestone-worker.yml │ ├── next-dev-version-worker.yml │ └── release-worker.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── README_DEV.md ├── SECURITY.md ├── appveyor.yml ├── master-ui-assets ├── header-logo-mobile.psd ├── header-logo.psd ├── images │ ├── Powered by Sauce Labs badges white.svg │ ├── browserstack-logo-600x315.png │ └── saucelabs-logo-600x315.png └── svg │ ├── favicon │ ├── 16x16.png │ ├── 24x24.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 64x64.png │ └── favicon-152.png │ ├── scdf-logo-teal-white.svg │ ├── scdf-logo-teal.svg │ ├── spring-logo-teal.svg │ └── spring-logo-transparent.svg ├── mvnw ├── mvnw.cmd ├── pom.xml ├── run-maven-build.sh ├── run-npm-e2e-browserstack.sh ├── run-npm-e2e-local.sh ├── run-npm-e2e-saucelabs.sh ├── run-npm-test-browserstack.sh ├── run-npm-test-saucelabs.sh ├── src └── main │ └── resources │ └── public │ └── favicon.ico └── ui ├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .prettierrc ├── .vscode └── launch.json ├── README.md ├── angular.json ├── cypress-dc-local.json ├── cypress-dc-local.yml ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── applications.spec.ts │ ├── streams.spec.ts │ └── tasks.spec.ts ├── plugins │ └── index.ts ├── support │ ├── commands.ts │ ├── index.ts │ └── navigation.ts ├── tsconfig.json └── videos │ └── applications.spec.ts.mp4 ├── package-lock.json ├── package.json ├── proxy.conf.json ├── src ├── app │ ├── about │ │ ├── about.module.ts │ │ ├── info │ │ │ ├── info.component.html │ │ │ ├── info.component.scss │ │ │ ├── info.component.spec.ts │ │ │ └── info.component.ts │ │ ├── signpost │ │ │ ├── signpost.component.html │ │ │ ├── signpost.component.spec.ts │ │ │ └── signpost.component.ts │ │ └── user │ │ │ ├── user.component.html │ │ │ ├── user.component.spec.ts │ │ │ └── user.component.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── apps │ │ ├── add │ │ │ ├── add.compoment.spec.ts │ │ │ ├── add.component.html │ │ │ ├── add.component.scss │ │ │ ├── add.component.ts │ │ │ ├── add.validator.spec.ts │ │ │ ├── add.validtor.ts │ │ │ ├── props │ │ │ │ ├── props.component.html │ │ │ │ ├── props.component.spec.ts │ │ │ │ └── props.component.ts │ │ │ ├── register │ │ │ │ ├── register.component.html │ │ │ │ ├── register.component.spec.ts │ │ │ │ └── register.component.ts │ │ │ ├── uri │ │ │ │ ├── uri.component.html │ │ │ │ ├── uri.component.spec.ts │ │ │ │ └── uri.component.ts │ │ │ └── website-starters │ │ │ │ ├── website-starters.component.html │ │ │ │ ├── website-starters.component.spec.ts │ │ │ │ └── website-starters.component.ts │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.spec.ts │ │ │ └── app.component.ts │ │ ├── apps-routing.module.ts │ │ ├── apps.component.html │ │ ├── apps.component.spec.ts │ │ ├── apps.component.ts │ │ ├── apps.module.ts │ │ ├── type.filter.spec.ts │ │ ├── type.filter.ts │ │ ├── unregister │ │ │ ├── unregister.component.html │ │ │ ├── unregister.component.spec.ts │ │ │ └── unregister.component.ts │ │ └── version │ │ │ ├── version.component.html │ │ │ ├── version.component.spec.ts │ │ │ └── version.component.ts │ ├── dev │ │ ├── dashboard │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.scss │ │ │ ├── dashboard.component.ts │ │ │ ├── stream-create │ │ │ │ ├── stream-create.component.html │ │ │ │ └── stream-create.component.ts │ │ │ └── task-create │ │ │ │ ├── task-create.component.html │ │ │ │ └── task-create.component.ts │ │ ├── dev-routing.module.ts │ │ └── dev.module.ts │ ├── flo │ │ ├── shared-flo.module.ts │ │ ├── shared │ │ │ ├── flo.scss │ │ │ ├── graph-view │ │ │ │ ├── graph-view.component.html │ │ │ │ ├── graph-view.component.spec.ts │ │ │ │ └── graph-view.component.ts │ │ │ ├── properties-groups │ │ │ │ ├── properties-groups-dialog.component.html │ │ │ │ ├── properties-groups-dialog.component.scss │ │ │ │ └── properties-groups-dialog.component.ts │ │ │ ├── properties │ │ │ │ ├── df.property.component.html │ │ │ │ ├── df.property.component.ts │ │ │ │ ├── properties-dialog.component.html │ │ │ │ ├── properties-dialog.component.scss │ │ │ │ ├── properties-dialog.component.ts │ │ │ │ ├── properties.group.component.html │ │ │ │ └── properties.group.component.ts │ │ │ ├── service │ │ │ │ ├── doc.service.ts │ │ │ │ ├── parser.service.ts │ │ │ │ ├── parser.spec.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── tokenizer.spec.ts │ │ │ │ └── tokenizer.ts │ │ │ └── support │ │ │ │ ├── app-metadata.ts │ │ │ │ ├── app-ui-property.ts │ │ │ │ ├── graph-node-properties-source.ts │ │ │ │ ├── node-component.spec.ts │ │ │ │ ├── node-component.ts │ │ │ │ ├── properties-group-model.ts │ │ │ │ ├── shape-component.ts │ │ │ │ ├── shared-shapes.ts │ │ │ │ ├── utils.ts │ │ │ │ └── view-utils.ts │ │ ├── stream-flo.module.ts │ │ ├── stream │ │ │ ├── component │ │ │ │ ├── create-component.spec.ts │ │ │ │ ├── create.component.ts │ │ │ │ ├── runtime-view.component.spec.ts │ │ │ │ ├── runtime-view.component.ts │ │ │ │ ├── view.component.spec.ts │ │ │ │ └── view.component.ts │ │ │ ├── content-assist.service.ts │ │ │ ├── dsl-sanitize.service.ts │ │ │ ├── editor.service.spec.ts │ │ │ ├── editor.service.ts │ │ │ ├── graph-to-text.spec.ts │ │ │ ├── graph-to-text.ts │ │ │ ├── instance-dot │ │ │ │ ├── instance-dot.component.html │ │ │ │ ├── instance-dot.component.scss │ │ │ │ ├── instance-dot.component.spec.ts │ │ │ │ └── instance-dot.component.ts │ │ │ ├── message-rate │ │ │ │ ├── message-rate.component.html │ │ │ │ ├── message-rate.component.scss │ │ │ │ ├── message-rate.component.spec.ts │ │ │ │ └── message-rate.component.ts │ │ │ ├── metamodel.service.ts │ │ │ ├── node-helper.service.ts │ │ │ ├── node │ │ │ │ ├── stream-node.component.html │ │ │ │ ├── stream-node.component.scss │ │ │ │ └── stream-node.component.ts │ │ │ ├── properties-editor.service.ts │ │ │ ├── properties │ │ │ │ ├── stream-properties-dialog.component.ts │ │ │ │ └── stream-properties-source.ts │ │ │ ├── render.service.spec.ts │ │ │ ├── render.service.ts │ │ │ ├── support │ │ │ │ ├── layout.ts │ │ │ │ ├── shapes.ts │ │ │ │ ├── utils.spec.ts │ │ │ │ ├── utils.ts │ │ │ │ └── view-helper.ts │ │ │ ├── text-to-graph.spec.ts │ │ │ └── text-to-graph.ts │ │ ├── task-flo.module.ts │ │ └── task │ │ │ ├── component │ │ │ ├── create.component.spec.ts │ │ │ ├── create.component.ts │ │ │ ├── view.component.spec.ts │ │ │ └── view.component.ts │ │ │ ├── content-assist.service.ts │ │ │ ├── editor.service.spec.ts │ │ │ ├── editor.service.ts │ │ │ ├── metamodel.service.spec.ts │ │ │ ├── metamodel.service.ts │ │ │ ├── model │ │ │ └── models.ts │ │ │ ├── node │ │ │ ├── task-node.component.html │ │ │ ├── task-node.component.scss │ │ │ └── task-node.component.ts │ │ │ ├── properties │ │ │ ├── task-properties-dialog-component.ts │ │ │ └── task-properties-source.ts │ │ │ ├── render.service.spec.ts │ │ │ ├── render.service.ts │ │ │ ├── support │ │ │ ├── layout.spec.ts │ │ │ ├── layout.ts │ │ │ └── shapes.ts │ │ │ ├── tools.service.spec.ts │ │ │ └── tools.service.ts │ ├── layout │ │ ├── layout.module.ts │ │ ├── logo │ │ │ └── logo.component.ts │ │ ├── nav │ │ │ ├── nav.component.html │ │ │ ├── nav.component.scss │ │ │ └── nav.component.ts │ │ └── theme │ │ │ ├── dark-theme.ts │ │ │ ├── default-theme.ts │ │ │ ├── theme.service.ts │ │ │ └── types.ts │ ├── manage │ │ ├── manage-routing.module.ts │ │ ├── manage.module.ts │ │ ├── records │ │ │ ├── action.filter.spec.ts │ │ │ ├── action.filter.ts │ │ │ ├── operation.filter.spec.ts │ │ │ ├── operation.filter.ts │ │ │ ├── records.component.html │ │ │ ├── records.component.spec.ts │ │ │ └── records.component.ts │ │ └── tools │ │ │ ├── cleanup │ │ │ ├── cleanup.component.html │ │ │ ├── cleanup.component.spec.ts │ │ │ └── cleanup.component.ts │ │ │ ├── stream │ │ │ ├── export.component.spec.ts │ │ │ ├── export.component.ts │ │ │ ├── import.component.spec.ts │ │ │ └── import.component.ts │ │ │ ├── task │ │ │ ├── export.component.spec.ts │ │ │ ├── export.component.ts │ │ │ ├── import.component.spec.ts │ │ │ └── import.component.ts │ │ │ ├── tools.component.html │ │ │ ├── tools.component.spec.ts │ │ │ └── tools.component.ts │ ├── reducers │ │ └── reducer.ts │ ├── security │ │ ├── component │ │ │ ├── authentication-required.component.spec.ts │ │ │ ├── authentication-required.component.ts │ │ │ ├── feature-disabled.component.spec.ts │ │ │ ├── feature-disabled.component.ts │ │ │ ├── roles-missing.component.spec.ts │ │ │ └── roles-missing.component.ts │ │ ├── directive │ │ │ └── role.directive.ts │ │ ├── security-routing.module.ts │ │ ├── security.module.ts │ │ ├── service │ │ │ ├── security.service.spec.ts │ │ │ └── security.service.ts │ │ ├── store │ │ │ ├── security.action.ts │ │ │ ├── security.effect.spec.ts │ │ │ ├── security.effect.ts │ │ │ ├── security.reducer.spec.ts │ │ │ └── security.reducer.ts │ │ └── support │ │ │ ├── security.guard.ts │ │ │ └── security.interceptor.ts │ ├── settings │ │ ├── settings-routing.module.ts │ │ ├── settings.module.ts │ │ ├── settings.service.ts │ │ ├── settings │ │ │ ├── settings.component.html │ │ │ ├── settings.component.spec.ts │ │ │ └── settings.component.ts │ │ └── store │ │ │ ├── settings.action.ts │ │ │ ├── settings.effect.spec.ts │ │ │ ├── settings.effect.ts │ │ │ ├── settings.reducer.spec.ts │ │ │ └── settings.reducer.ts │ ├── shared │ │ ├── api │ │ │ ├── about.service.spec.ts │ │ │ ├── about.service.ts │ │ │ ├── app.service.spec.ts │ │ │ ├── app.service.ts │ │ │ ├── job.service.spec.ts │ │ │ ├── job.service.ts │ │ │ ├── record.service.spec.ts │ │ │ ├── record.service.ts │ │ │ ├── runtime.service.spec.ts │ │ │ ├── runtime.service.ts │ │ │ ├── schedule.service.spec.ts │ │ │ ├── schedule.service.ts │ │ │ ├── stream.service.spec.ts │ │ │ ├── stream.service.ts │ │ │ ├── task.service.spec.ts │ │ │ └── task.service.ts │ │ ├── component │ │ │ ├── card │ │ │ │ ├── card.component.html │ │ │ │ └── card.component.ts │ │ │ ├── confirm │ │ │ │ ├── confirm.component.html │ │ │ │ └── confirm.component.ts │ │ │ ├── datagrid │ │ │ │ └── datagrid.component.ts │ │ │ ├── key-value │ │ │ │ ├── key-value.component.scss │ │ │ │ ├── key-value.component.spec.ts │ │ │ │ ├── key-value.component.ts │ │ │ │ ├── key-value.interface.ts │ │ │ │ ├── key-value.validator.spec.ts │ │ │ │ └── key-value.validator.ts │ │ │ ├── search │ │ │ │ ├── search.component.html │ │ │ │ ├── search.component.scss │ │ │ │ ├── search.component.spec.ts │ │ │ │ └── search.component.ts │ │ │ ├── stream-dsl │ │ │ │ ├── stream-dsl.component.spec.ts │ │ │ │ └── stream-dsl.component.ts │ │ │ └── toast │ │ │ │ ├── toast.component.html │ │ │ │ ├── toast.component.scss │ │ │ │ └── toast.component.ts │ │ ├── directive │ │ │ ├── auto-resize.directive.ts │ │ │ ├── focus.directive.ts │ │ │ └── tippy.directive.ts │ │ ├── filter │ │ │ └── date │ │ │ │ ├── date.filter.spec.ts │ │ │ │ └── date.filter.ts │ │ ├── grafana │ │ │ ├── grafana.directive.spec.ts │ │ │ ├── grafana.directive.ts │ │ │ ├── grafana.service.spec.ts │ │ │ └── grafana.service.ts │ │ ├── model │ │ │ ├── app.model.ts │ │ │ ├── context.model.ts │ │ │ ├── detailed-app.model.ts │ │ │ ├── error.model.ts │ │ │ ├── instance.model.ts │ │ │ ├── job.model.ts │ │ │ ├── metrics.model.ts │ │ │ ├── page.model.ts │ │ │ ├── platform.model.ts │ │ │ ├── record.model.ts │ │ │ ├── runtime.model.ts │ │ │ ├── schedule.model.ts │ │ │ ├── security.model.ts │ │ │ ├── setting.model.ts │ │ │ ├── stream.model.ts │ │ │ ├── task-execution.model.ts │ │ │ └── task.model.ts │ │ ├── pipe │ │ │ ├── capitalize.pipe.ts │ │ │ ├── datagrid-column.pipe.ts │ │ │ ├── datetime.pipe.ts │ │ │ ├── duration.pipe.ts │ │ │ └── order-by.pipe.ts │ │ ├── service │ │ │ ├── clipboard-copy.service.ts │ │ │ ├── context.service.ts │ │ │ ├── group.service.ts │ │ │ ├── import-export.service.ts │ │ │ ├── local-storage.service.ts │ │ │ ├── logger.service.ts │ │ │ ├── modal.service.ts │ │ │ └── notification.service.ts │ │ ├── shared.module.ts │ │ ├── store │ │ │ ├── about.action.ts │ │ │ ├── about.reducer.ts │ │ │ ├── about.support.ts │ │ │ ├── context.action.ts │ │ │ └── context.reducer.ts │ │ ├── support │ │ │ ├── encoder.utils.ts │ │ │ ├── error.utils.ts │ │ │ └── http.utils.ts │ │ └── wavefront │ │ │ ├── wavefront.directive.spec.ts │ │ │ ├── wavefront.directive.ts │ │ │ ├── wavefront.service.spec.ts │ │ │ └── wavefront.service.ts │ ├── streams │ │ ├── runtime │ │ │ ├── details │ │ │ │ ├── details.component.html │ │ │ │ ├── details.component.scss │ │ │ │ ├── details.component.spec.ts │ │ │ │ └── details.component.ts │ │ │ ├── runtime.component.html │ │ │ ├── runtime.component.spec.ts │ │ │ └── runtime.component.ts │ │ ├── streams-routing.module.ts │ │ ├── streams.module.ts │ │ └── streams │ │ │ ├── clone │ │ │ ├── clone.component.html │ │ │ ├── clone.component.spec.ts │ │ │ └── clone.component.ts │ │ │ ├── create │ │ │ ├── create.component.html │ │ │ ├── create.component.spec.ts │ │ │ └── create.component.ts │ │ │ ├── deploy │ │ │ ├── builder │ │ │ │ ├── builder.component.html │ │ │ │ ├── builder.component.scss │ │ │ │ ├── builder.component.spec.ts │ │ │ │ ├── builder.component.ts │ │ │ │ └── errors │ │ │ │ │ ├── errors.component.html │ │ │ │ │ ├── errors.component.scss │ │ │ │ │ ├── errors.component.spec.ts │ │ │ │ │ └── errors.component.ts │ │ │ ├── deploy.component.html │ │ │ ├── deploy.component.ts │ │ │ ├── free-text │ │ │ │ ├── free-text.component.html │ │ │ │ ├── free-text.component.scss │ │ │ │ ├── free-text.component.spec.ts │ │ │ │ └── free-text.component.ts │ │ │ ├── stream-deploy.component.spec.ts │ │ │ ├── stream-deploy.validator.spec.ts │ │ │ └── stream-deploy.validator.ts │ │ │ ├── destroy │ │ │ ├── destroy.component.html │ │ │ ├── destroy.component.spec.ts │ │ │ └── destroy.component.ts │ │ │ ├── multi-deploy │ │ │ ├── multi-deploy.component.html │ │ │ ├── multi-deploy.component.scss │ │ │ ├── multi-deploy.component.spec.ts │ │ │ └── multi-deploy.component.ts │ │ │ ├── rollback │ │ │ ├── rollback.component.html │ │ │ ├── rollback.component.spec.ts │ │ │ └── rollback.component.ts │ │ │ ├── scale │ │ │ ├── scale.component.html │ │ │ ├── scale.component.scss │ │ │ ├── scale.component.spec.ts │ │ │ └── scale.component.ts │ │ │ ├── status │ │ │ ├── status.component.spec.ts │ │ │ └── status.component.ts │ │ │ ├── stream-deploy.service.spec.ts │ │ │ ├── stream-deploy.service.ts │ │ │ ├── stream │ │ │ ├── stream.component.html │ │ │ ├── stream.component.spec.ts │ │ │ └── stream.component.ts │ │ │ ├── streams.component.html │ │ │ ├── streams.component.spec.ts │ │ │ ├── streams.component.ts │ │ │ └── undeploy │ │ │ ├── undeploy.component.html │ │ │ ├── undeploy.component.spec.ts │ │ │ └── undeploy.component.ts │ ├── tasks-jobs │ │ ├── executions │ │ │ ├── cleanup │ │ │ │ ├── cleanup.component.html │ │ │ │ ├── cleanup.component.spec.ts │ │ │ │ └── cleanup.component.ts │ │ │ ├── execution │ │ │ │ ├── execution.component.html │ │ │ │ ├── execution.component.spec.ts │ │ │ │ ├── execution.component.ts │ │ │ │ └── log │ │ │ │ │ └── log.component.ts │ │ │ ├── executions.component.html │ │ │ ├── executions.component.spec.ts │ │ │ ├── executions.component.ts │ │ │ └── stop │ │ │ │ ├── stop.component.html │ │ │ │ ├── stop.component.spec.ts │ │ │ │ └── stop.component.ts │ │ ├── jobs │ │ │ ├── execution │ │ │ │ ├── execution.component.html │ │ │ │ ├── execution.component.spec.ts │ │ │ │ └── execution.component.ts │ │ │ ├── jobs.component.html │ │ │ ├── jobs.component.spec.ts │ │ │ ├── jobs.component.ts │ │ │ └── step │ │ │ │ ├── step.component.html │ │ │ │ ├── step.component.spec.ts │ │ │ │ └── step.component.ts │ │ ├── schedules │ │ │ ├── create │ │ │ │ ├── create.component.html │ │ │ │ ├── create.component.spec.ts │ │ │ │ ├── create.component.ts │ │ │ │ ├── create.validator.spec.ts │ │ │ │ └── create.validator.ts │ │ │ ├── destroy │ │ │ │ ├── destroy.component.html │ │ │ │ ├── destroy.component.spec.ts │ │ │ │ └── destroy.component.ts │ │ │ ├── platform.filter.ts │ │ │ ├── schedule │ │ │ │ ├── schedule.component.html │ │ │ │ ├── schedule.component.spec.ts │ │ │ │ └── schedule.component.ts │ │ │ ├── schedules.component.html │ │ │ ├── schedules.component.spec.ts │ │ │ └── schedules.component.ts │ │ ├── tasks-jobs-routing.module.ts │ │ ├── tasks-jobs.module.ts │ │ └── tasks │ │ │ ├── cleanup │ │ │ ├── cleanup.component.html │ │ │ ├── cleanup.component.spec.ts │ │ │ └── cleanup.component.ts │ │ │ ├── clone │ │ │ ├── clone.component.html │ │ │ ├── clone.component.spec.ts │ │ │ └── clone.component.ts │ │ │ ├── create │ │ │ ├── create.component.html │ │ │ ├── create.component.spec.ts │ │ │ └── create.component.ts │ │ │ ├── destroy │ │ │ ├── destroy.component.html │ │ │ ├── destroy.component.spec.ts │ │ │ └── destroy.component.ts │ │ │ ├── launch │ │ │ ├── builder │ │ │ │ ├── builder.component.html │ │ │ │ ├── builder.component.scss │ │ │ │ ├── builder.component.spec.ts │ │ │ │ ├── builder.component.ts │ │ │ │ └── errors │ │ │ │ │ ├── errors.component.html │ │ │ │ │ ├── errors.component.scss │ │ │ │ │ ├── errors.component.spec.ts │ │ │ │ │ └── errors.component.ts │ │ │ ├── free-text │ │ │ │ ├── free-text.component.html │ │ │ │ ├── free-text.component.scss │ │ │ │ ├── free-text.component.spec.ts │ │ │ │ └── free-text.component.ts │ │ │ ├── launch.component.html │ │ │ ├── launch.component.spec.ts │ │ │ ├── launch.component.ts │ │ │ ├── task-launch.service.ts │ │ │ └── task-launch.validator.ts │ │ │ ├── task-prop.validator.spec.ts │ │ │ ├── task-prop.validator.ts │ │ │ ├── task │ │ │ ├── task.component.html │ │ │ ├── task.component.spec.ts │ │ │ └── task.component.ts │ │ │ ├── tasks.component.html │ │ │ ├── tasks.component.spec.ts │ │ │ └── tasks.component.ts │ ├── tests │ │ ├── api │ │ │ ├── about.service.mock.ts │ │ │ ├── app.service.mock.ts │ │ │ ├── content-assist.service.spec.ts │ │ │ ├── job.service.mock.ts │ │ │ ├── record.service.mock.ts │ │ │ ├── runtime.service.mock.spec.ts │ │ │ ├── schedule.service.mock.ts │ │ │ ├── security.service.mock.ts │ │ │ ├── stream.service.mock.ts │ │ │ └── task.service.mock.ts │ │ ├── data │ │ │ ├── about.ts │ │ │ ├── app.ts │ │ │ ├── job.ts │ │ │ ├── record.ts │ │ │ ├── runtime.ts │ │ │ ├── schedule.ts │ │ │ ├── security.ts │ │ │ ├── stream-deploy.ts │ │ │ ├── stream.ts │ │ │ └── task.ts │ │ └── service │ │ │ ├── app.service.mock.ts │ │ │ ├── context.service.mock.ts │ │ │ ├── grafana.service.mock.ts │ │ │ ├── group.service.mock.ts │ │ │ ├── import-export.service.mock.ts │ │ │ ├── notification.service.mock.ts │ │ │ ├── settings.service.mock.ts │ │ │ ├── stream-deploy.service.mock.ts │ │ │ ├── stream.service.mock.ts │ │ │ ├── task-launch.service.mock.ts │ │ │ └── task-tools.service.mock.ts │ └── url-utilities.service.ts ├── assets │ ├── .gitkeep │ ├── i18n │ │ ├── de.json │ │ ├── en.json │ │ └── ru.json │ └── img │ │ ├── api.png │ │ ├── app.svg │ │ ├── chevron-down-white.svg │ │ ├── chevron-down.svg │ │ ├── chevron-left-white.svg │ │ ├── chevron-left.svg │ │ ├── cog.svg │ │ ├── delete.svg │ │ ├── documentation.png │ │ ├── error.svg │ │ ├── forum.png │ │ ├── github.png │ │ ├── processor.svg │ │ ├── project-page.png │ │ ├── shell.png │ │ ├── sink.svg │ │ ├── source.svg │ │ ├── tap.svg │ │ ├── tracker.png │ │ └── unknown.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma-browserstack.conf.js ├── karma-saucelabs.conf.js ├── karma.conf.js ├── logout-success-oauth.html ├── main.ts ├── polyfills.ts ├── scss │ └── styles.scss ├── styles.scss ├── test.ts ├── tsconfig.app.json └── tsconfig.spec.json └── tsconfig.json /.github/dco.yml: -------------------------------------------------------------------------------- 1 | require: 2 | members: false 3 | -------------------------------------------------------------------------------- /.github/workflows/ci-e2e.yml: -------------------------------------------------------------------------------- 1 | name: CI E2E 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | e2e: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v2 12 | with: 13 | node-version: '14' 14 | - name: Start scdf 15 | working-directory: ui 16 | run: | 17 | docker-compose -f cypress-dc-local.yml up -d 18 | - uses: cypress-io/github-action@v2 19 | with: 20 | working-directory: ui 21 | config-file: cypress-dc-local.json 22 | browser: chrome 23 | headless: true 24 | - uses: actions/upload-artifact@v4 25 | if: failure() 26 | with: 27 | name: cypress-artifacts 28 | path: | 29 | ui/cypress/screenshots/ 30 | ui/cypress/videos/ 31 | -------------------------------------------------------------------------------- /.github/workflows/ci-pr.yml: -------------------------------------------------------------------------------- 1 | name: CI PRs 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | # cache maven repo 12 | - uses: actions/cache@v3 13 | with: 14 | path: ~/.m2/repository 15 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 16 | restore-keys: | 17 | ${{ runner.os }}-m2- 18 | # jdk8 19 | - uses: actions/setup-java@v1 20 | with: 21 | java-version: 1.8 22 | # build 23 | - name: Build 24 | run: | 25 | ./mvnw -U -B clean package 26 | # clean m2 cache 27 | - name: Clean cache 28 | run: | 29 | find ~/.m2/repository -type d -name '*SNAPSHOT' | xargs rm -fr 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths-ignore: 7 | - '.github/**' 8 | 9 | jobs: 10 | build: 11 | if: github.repository_owner == 'spring-cloud' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | # cache maven repo 16 | - uses: actions/cache@v3 17 | with: 18 | path: ~/.m2/repository 19 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 20 | restore-keys: | 21 | ${{ runner.os }}-m2- 22 | # jdk8 23 | - uses: actions/setup-java@v1 24 | with: 25 | java-version: 1.8 26 | # jfrog cli 27 | - uses: jfrog/setup-jfrog-cli@v3 28 | env: 29 | JF_URL: 'https://repo.spring.io' 30 | JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} 31 | # setup frog cli 32 | - name: Configure JFrog Cli 33 | run: | 34 | jfrog mvnc --use-wrapper \ 35 | --server-id-resolve=${{ vars.JF_SERVER_ID }} \ 36 | --server-id-deploy=${{ vars.JF_SERVER_ID }} \ 37 | --repo-resolve-releases=libs-milestone \ 38 | --repo-resolve-snapshots=libs-snapshot \ 39 | --repo-deploy-releases=libs-release-local \ 40 | --repo-deploy-snapshots=libs-snapshot-local 41 | echo JFROG_CLI_BUILD_NAME=spring-cloud-dataflow-ui-main >> $GITHUB_ENV 42 | echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV 43 | # build and publish 44 | - name: Build and Publish 45 | run: | 46 | jfrog mvn -U -B clean install 47 | jfrog rt build-publish 48 | # clean m2 cache 49 | - name: Clean cache 50 | run: | 51 | find ~/.m2/repository -type d -name '*SNAPSHOT' | xargs rm -fr 52 | -------------------------------------------------------------------------------- /.github/workflows/label-manage.yml: -------------------------------------------------------------------------------- 1 | name: Labels Manage 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - '.github/labels-manage.yml' 9 | - '.github/workflows/label-manage.yml' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | labeler: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Manage Labels 18 | uses: crazy-max/ghaction-github-labeler@v3 19 | with: 20 | github-token: ${{ secrets.GITHUB_TOKEN }} 21 | yaml-file: .github/labels-manage.yml 22 | dry-run: false 23 | skip-delete: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ui/.bower_cache 2 | ui/bower_components 3 | ui/node 4 | ui/node_modules 5 | ui/app/lib/ 6 | ui/app/styles/main.css 7 | ui/app/styles/main.css.map 8 | ui/.tmp 9 | ui/documentation/ 10 | ui/dist 11 | ui/tmp 12 | ui/out-tsc 13 | ui/.c9/ 14 | ui/.angular/ 15 | 16 | ui/.sass-cache 17 | ui/connect.lock 18 | ui/coverage 19 | ui/libpeerconnection.log 20 | ui/npm-debug.log 21 | ui/testem.log 22 | ui/local.log 23 | ui/typings 24 | 25 | 26 | src/main/resources/public/dashboard 27 | target/ 28 | .classpath 29 | .project 30 | .settings/ 31 | .idea 32 | *.iml 33 | *.ipr 34 | *.iws 35 | npm-debug.log 36 | *.launch 37 | *.sublime-workspace 38 | .DS_Store 39 | Thumbs.db 40 | .jfrog/ 41 | 42 | # IDE - VSCode main 43 | .vscode/* 44 | !.vscode/settings.json 45 | !.vscode/tasks.json 46 | !.vscode/launch.json 47 | !.vscode/extensions.json 48 | 49 | # IDE - VSCode ui 50 | ui/.vscode/* 51 | !ui/.vscode/settings.json 52 | !ui/.vscode/tasks.json 53 | !ui/.vscode/launch.json 54 | !ui/.vscode/extensions.json 55 | 56 | # Cypress e2e tests 57 | ui/cypress/videos 58 | ui/cypress/downloads 59 | ui/cypress/screenshots 60 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | ## Reporting a Vulnerability 3 | 4 | If you think you have found a security vulnerability, please **DO NOT** disclose it publicly until we’ve had a chance to fix it. 5 | Please don’t report security vulnerabilities using GitHub issues, instead head over to https://spring.io/security-policy and learn how to disclose them responsibly. 6 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | skip_tags: true 3 | clone_depth: 10 4 | environment: 5 | matrix: 6 | - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 7 | branches: 8 | only: 9 | - master 10 | except: 11 | - gh-pages 12 | os: Windows Server 2012 13 | install: 14 | - cmd: SET PATH=C:\maven\apache-maven-3.2.5\bin;%JAVA_HOME%\bin;%PATH:C:\Ruby193\bin;=% 15 | - cmd: SET MAVEN_OPTS=-XX:MaxPermSize=2g -Xmx4g 16 | - cmd: SET JAVA_OPTS=-XX:MaxPermSize=2g -Xmx4g 17 | - cmd: java -version 18 | build_script: 19 | - mvnw clean install --batch-mode -Dmaven.test.skip=false 20 | cache: 21 | - C:\Users\appveyor\.m2 22 | -------------------------------------------------------------------------------- /master-ui-assets/header-logo-mobile.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/master-ui-assets/header-logo-mobile.psd -------------------------------------------------------------------------------- /master-ui-assets/header-logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/master-ui-assets/header-logo.psd -------------------------------------------------------------------------------- /master-ui-assets/images/browserstack-logo-600x315.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/master-ui-assets/images/browserstack-logo-600x315.png -------------------------------------------------------------------------------- /master-ui-assets/images/saucelabs-logo-600x315.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/master-ui-assets/images/saucelabs-logo-600x315.png -------------------------------------------------------------------------------- /master-ui-assets/svg/favicon/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/master-ui-assets/svg/favicon/16x16.png -------------------------------------------------------------------------------- /master-ui-assets/svg/favicon/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/master-ui-assets/svg/favicon/24x24.png -------------------------------------------------------------------------------- /master-ui-assets/svg/favicon/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/master-ui-assets/svg/favicon/32x32.png -------------------------------------------------------------------------------- /master-ui-assets/svg/favicon/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/master-ui-assets/svg/favicon/48x48.png -------------------------------------------------------------------------------- /master-ui-assets/svg/favicon/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/master-ui-assets/svg/favicon/64x64.png -------------------------------------------------------------------------------- /master-ui-assets/svg/favicon/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/master-ui-assets/svg/favicon/favicon-152.png -------------------------------------------------------------------------------- /master-ui-assets/svg/spring-logo-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /run-maven-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | mvn clean package 4 | -------------------------------------------------------------------------------- /run-npm-e2e-browserstack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | cd ui 4 | mkdir .docker 5 | cd .docker 6 | curl -O https://raw.githubusercontent.com/spring-cloud/spring-cloud-dataflow/v$DATAFLOW_VERSION/spring-cloud-dataflow-server/docker-compose.yml 7 | docker-compose up --no-start 8 | cd .. 9 | npm install 10 | npm run e2e-browserstack-local 11 | cd .. 12 | -------------------------------------------------------------------------------- /run-npm-e2e-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | cd ui 4 | mkdir .docker 5 | cd .docker 6 | curl -O https://raw.githubusercontent.com/spring-cloud/spring-cloud-dataflow/v$DATAFLOW_VERSION/spring-cloud-dataflow-server/docker-compose.yml 7 | docker-compose up --no-start 8 | cd .. 9 | npm install 10 | npm run e2e 11 | cd .. 12 | -------------------------------------------------------------------------------- /run-npm-e2e-saucelabs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | cd ui 4 | mkdir .docker 5 | cd .docker 6 | curl -O https://raw.githubusercontent.com/spring-cloud/spring-cloud-dataflow/v$DATAFLOW_VERSION/spring-cloud-dataflow-server/docker-compose.yml 7 | docker-compose up --no-start 8 | cd .. 9 | npm install 10 | npm run e2e-saucelabs-local 11 | cd .. 12 | -------------------------------------------------------------------------------- /run-npm-test-browserstack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | cd ui 4 | npm install 5 | npm run test-browserstack-local 6 | cd .. -------------------------------------------------------------------------------- /run-npm-test-saucelabs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | cd ui 4 | npm install 5 | npm run test-saucelabs-local 6 | cd .. -------------------------------------------------------------------------------- /src/main/resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/src/main/resources/public/favicon.ico -------------------------------------------------------------------------------- /ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. 13 | not ios_saf 15.2-15.3 14 | not safari 15.2-15.3 -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | quote_type = single 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid", 5 | "printWidth": 120, 6 | "bracketSpacing": false, 7 | "overrides": [ 8 | { 9 | "files": "*.{component.html}", 10 | "options": { "parser": "angular" } 11 | }, 12 | { 13 | "files": "*.{html}", 14 | "options": { "parser": "html" } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /ui/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Launch Chrome against localhost", 8 | "url": "http://localhost:4200", 9 | "webRoot": "${workspaceFolder}" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /ui/cypress-dc-local.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrationFolder": "cypress/integration", 3 | "supportFile": "cypress/support/index.ts", 4 | "videosFolder": "cypress/videos", 5 | "screenshotsFolder": "cypress/screenshots", 6 | "pluginsFile": "cypress/plugins/index.ts", 7 | "fixturesFolder": "cypress/fixtures", 8 | "baseUrl": "http://localhost:9393/dashboard/index.html", 9 | "chromeWebSecurity": false, 10 | "defaultCommandTimeout": 60000, 11 | "pageLoadTimeout": 60000, 12 | "viewportWidth": 1400, 13 | "viewportHeight": 800 14 | } 15 | -------------------------------------------------------------------------------- /ui/cypress-dc-local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | rabbitmq-server: 6 | image: rabbitmq:management 7 | ports: 8 | - "5672:5672" 9 | - "15672:15672" 10 | 11 | skipper-server: 12 | image: springcloud/spring-cloud-skipper-server:2.9.0-SNAPSHOT 13 | ports: 14 | - "7577:7577" 15 | - "8888:8888" 16 | - "8889:8889" 17 | - "20000-20099:20000-20099" 18 | environment: 19 | - SPRING_CLOUD_SKIPPER_SERVER_PLATFORM_LOCAL_ACCOUNTS_DEFAULT_PORTRANGE_LOW=20000 20 | - SPRING_CLOUD_SKIPPER_SERVER_PLATFORM_LOCAL_ACCOUNTS_DEFAULT_PORTRANGE_HIGH=20100 21 | 22 | dataflow-server: 23 | image: springcloud/spring-cloud-dataflow-server:2.10.0-SNAPSHOT 24 | ports: 25 | - "9393:9393" 26 | - "20100-20199:20100-20199" 27 | environment: 28 | - MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=* 29 | - SPRING_CLOUD_DATAFLOW_FEATURES_SCHEDULES_ENABLED=true 30 | - SPRING_CLOUD_SKIPPER_CLIENT_SERVER_URI=http://skipper-server:7577/api 31 | - SPRING_CLOUD_DATAFLOW_APPLICATIONPROPERTIES_STREAM_SPRING_RABBITMQ_ADDRESSES=rabbitmq-server:5672 32 | -------------------------------------------------------------------------------- /ui/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrationFolder": "cypress/integration", 3 | "supportFile": "cypress/support/index.ts", 4 | "videosFolder": "cypress/videos", 5 | "screenshotsFolder": "cypress/screenshots", 6 | "pluginsFile": "cypress/plugins/index.ts", 7 | "fixturesFolder": "cypress/fixtures", 8 | "baseUrl": "http://localhost:4200", 9 | "chromeWebSecurity": false, 10 | "defaultCommandTimeout": 60000, 11 | "pageLoadTimeout": 60000, 12 | "viewportWidth": 1400, 13 | "viewportHeight": 800 14 | } 15 | -------------------------------------------------------------------------------- /ui/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /ui/cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | // Plugins enable you to tap into, modify, or extend the internal behavior of Cypress 2 | // For more info, visit https://on.cypress.io/plugins-api 3 | module.exports = (on, config) => {}; 4 | -------------------------------------------------------------------------------- /ui/cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | // Import commands.js 2 | 3 | import './navigation'; 4 | import './commands'; 5 | -------------------------------------------------------------------------------- /ui/cypress/support/navigation.ts: -------------------------------------------------------------------------------- 1 | declare namespace Cypress { 2 | interface Chainable { 3 | apps(): void; 4 | streams(): void; 5 | tasks(): void; 6 | taskExecutions(): void; 7 | jobExecutions(): void; 8 | schedules(): void; 9 | auditRecords(): void; 10 | tools(): void; 11 | } 12 | } 13 | 14 | Cypress.Commands.add('apps', () => { 15 | cy.visit(Cypress.config('baseUrl')); 16 | cy.get('a[data-cy=navApplications]').click(); 17 | cy.get('clr-spinner').should('not.exist'); 18 | }); 19 | 20 | Cypress.Commands.add('streams', () => { 21 | cy.visit(Cypress.config('baseUrl')); 22 | cy.get('a[data-cy=navStreams]').click(); 23 | cy.get('clr-spinner').should('not.exist'); 24 | }); 25 | 26 | Cypress.Commands.add('tasks', () => { 27 | cy.visit(Cypress.config('baseUrl')); 28 | cy.get('a[data-cy=navTasks]').click(); 29 | cy.get('clr-spinner').should('not.exist'); 30 | }); 31 | 32 | Cypress.Commands.add('taskExecutions', () => { 33 | cy.visit(Cypress.config('baseUrl')); 34 | cy.get('a[data-cy=navTaskExecutions]').click(); 35 | cy.get('clr-spinner').should('not.exist'); 36 | }); 37 | 38 | Cypress.Commands.add('jobExecutions', () => { 39 | cy.visit(Cypress.config('baseUrl')); 40 | cy.get('a[data-cy=navJobExecutions]').click(); 41 | cy.get('clr-spinner').should('not.exist'); 42 | }); 43 | 44 | Cypress.Commands.add('schedules', () => { 45 | cy.visit(Cypress.config('baseUrl')); 46 | cy.get('a[data-cy=navSchedules]').click(); 47 | cy.get('clr-spinner').should('not.exist'); 48 | }); 49 | 50 | Cypress.Commands.add('auditRecords', () => { 51 | cy.visit(Cypress.config('baseUrl')); 52 | cy.get('a[data-cy=navAuditRecords]').click(); 53 | cy.get('clr-spinner').should('not.exist'); 54 | }); 55 | 56 | Cypress.Commands.add('tools', () => { 57 | cy.visit(Cypress.config('baseUrl')); 58 | cy.get('a[data-cy=navTools]').click(); 59 | cy.get('clr-spinner').should('not.exist'); 60 | }); 61 | -------------------------------------------------------------------------------- /ui/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "sourceMap": false, 6 | "types": ["cypress"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ui/cypress/videos/applications.spec.ts.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/ui/cypress/videos/applications.spec.ts.mp4 -------------------------------------------------------------------------------- /ui/src/app/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {InfoComponent} from './info/info.component'; 4 | import {ClarityModule} from '@clr/angular'; 5 | import {SignpostComponent} from './signpost/signpost.component'; 6 | import {UserComponent} from './user/user.component'; 7 | import {StoreModule} from '@ngrx/store'; 8 | import * as fromAbout from '../shared/store/about.reducer'; 9 | import {TranslateModule} from '@ngx-translate/core'; 10 | 11 | @NgModule({ 12 | declarations: [InfoComponent, SignpostComponent, UserComponent], 13 | imports: [ 14 | CommonModule, 15 | ClarityModule, 16 | StoreModule.forFeature(fromAbout.aboutFeatureKey, fromAbout.reducer), 17 | TranslateModule 18 | ], 19 | exports: [SignpostComponent, UserComponent] 20 | }) 21 | export class AboutModule {} 22 | -------------------------------------------------------------------------------- /ui/src/app/about/info/info.component.scss: -------------------------------------------------------------------------------- 1 | .modal-about { 2 | clr-icon { 3 | color: var(--clr-global-font-color); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/about/info/info.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; 2 | import {AboutState} from '../../shared/store/about.reducer'; 3 | import {AboutService} from '../../shared/api/about.service'; 4 | import {NotificationService} from '../../shared/service/notification.service'; 5 | import {ClipboardCopyService} from '../../shared/service/clipboard-copy.service'; 6 | import {JsonPipe} from '@angular/common'; 7 | import {TranslateService} from '@ngx-translate/core'; 8 | 9 | @Component({ 10 | selector: 'app-about-info', 11 | templateUrl: './info.component.html', 12 | styleUrls: ['./info.component.scss'] 13 | }) 14 | export class InfoComponent implements OnInit { 15 | loading = true; 16 | about: AboutState; 17 | @Input() isOpen = false; 18 | @ViewChild('container', {read: ElementRef, static: true}) container: ElementRef; 19 | 20 | constructor( 21 | private aboutService: AboutService, 22 | private clipboardCopyService: ClipboardCopyService, 23 | private notificationService: NotificationService, 24 | private translate: TranslateService 25 | ) {} 26 | 27 | ngOnInit(): void { 28 | this.aboutService.getAbout().subscribe( 29 | (about: AboutState) => { 30 | this.about = about; 31 | this.loading = false; 32 | }, 33 | error => { 34 | this.notificationService.error(this.translate.instant('commons.message.error'), error); 35 | this.loading = false; 36 | } 37 | ); 38 | } 39 | 40 | copyToClipboard(): void { 41 | if (this.about) { 42 | this.clipboardCopyService.executeCopy(new JsonPipe().transform(this.about), this.container.nativeElement); 43 | this.notificationService.success( 44 | this.translate.instant('commons.copyToClipboard'), 45 | this.translate.instant('about.info.message.copyContent') 46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ui/src/app/about/signpost/signpost.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ViewChild} from '@angular/core'; 2 | import {InfoComponent} from '../info/info.component'; 3 | import {ClrSignpostContent} from '@clr/angular'; 4 | import {AboutState} from '../../shared/store/about.reducer'; 5 | import {AboutService} from '../../shared/api/about.service'; 6 | import {TranslateService} from '@ngx-translate/core'; 7 | import {NotificationService} from '../../shared/service/notification.service'; 8 | 9 | @Component({ 10 | selector: 'app-about-signpost', 11 | templateUrl: './signpost.component.html' 12 | }) 13 | export class SignpostComponent implements OnInit { 14 | loading = true; 15 | about: AboutState; 16 | @ViewChild('infoModal', {static: true}) infoModal: InfoComponent; 17 | @ViewChild('signpost') signpost: ClrSignpostContent; 18 | 19 | constructor( 20 | private aboutService: AboutService, 21 | private notificationService: NotificationService, 22 | private translate: TranslateService 23 | ) {} 24 | 25 | ngOnInit(): void { 26 | this.aboutService.getAbout().subscribe( 27 | (about: AboutState) => { 28 | this.about = about; 29 | this.loading = false; 30 | }, 31 | error => { 32 | this.notificationService.error(this.translate.instant('commons.message.error'), error); 33 | this.loading = false; 34 | } 35 | ); 36 | } 37 | 38 | more(): void { 39 | this.infoModal.isOpen = true; 40 | this.signpost.close(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/app/about/user/user.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 | 8 | {{ 'about.user.logOut' | translate }} 9 | 10 | 11 |
12 | 13 | 14 |
15 | {{ 'about.user.logIn' | translate }} 16 |
17 |
18 | -------------------------------------------------------------------------------- /ui/src/app/about/user/user.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; 2 | import {FormsModule} from '@angular/forms'; 3 | import {ClarityModule} from '@clr/angular'; 4 | import {RouterTestingModule} from '@angular/router/testing'; 5 | import {UserComponent} from './user.component'; 6 | import {NotificationServiceMock} from '../../tests/service/notification.service.mock'; 7 | import {SecurityServiceMock} from '../../../app/tests/api/security.service.mock'; 8 | import {TranslateTestingModule} from 'ngx-translate-testing'; 9 | import TRANSLATIONS from '../../../assets/i18n/en.json'; 10 | 11 | describe('UserComponent', () => { 12 | let component: UserComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(waitForAsync(() => { 16 | TestBed.configureTestingModule({ 17 | declarations: [UserComponent], 18 | imports: [ 19 | FormsModule, 20 | ClarityModule, 21 | TranslateTestingModule.withTranslations('en', TRANSLATIONS), 22 | RouterTestingModule.withRoutes([]) 23 | ], 24 | providers: [SecurityServiceMock.provider, NotificationServiceMock.provider] 25 | }).compileComponents(); 26 | })); 27 | 28 | beforeEach(() => { 29 | NotificationServiceMock.mock.clearAll(); 30 | fixture = TestBed.createComponent(UserComponent); 31 | component = fixture.componentInstance; 32 | fixture.detectChanges(); 33 | }); 34 | 35 | it('should create', () => { 36 | expect(component).toBeTruthy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /ui/src/app/about/user/user.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | import {SecurityService} from '../../security/service/security.service'; 4 | import {UrlUtilities} from '../../url-utilities.service'; 5 | import {NotificationService} from '../../shared/service/notification.service'; 6 | import {TranslateService} from '@ngx-translate/core'; 7 | 8 | @Component({ 9 | selector: 'app-user', 10 | templateUrl: './user.component.html' 11 | }) 12 | export class UserComponent { 13 | loggedinUser$ = this.securityService.loggedinUser(); 14 | baseApiUrl = UrlUtilities.calculateBaseApiUrl(); 15 | 16 | constructor( 17 | private securityService: SecurityService, 18 | private router: Router, 19 | private notificationService: NotificationService, 20 | private translate: TranslateService 21 | ) {} 22 | 23 | logout(): void { 24 | this.securityService.logout().subscribe( 25 | security => { 26 | this.router.navigate(['/']); 27 | }, 28 | error => { 29 | this.notificationService.error(this.translate.instant('commons.message.error'), error); 30 | } 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | pathMatch: 'full', 8 | redirectTo: 'apps' 9 | } 10 | ]; 11 | @NgModule({ 12 | imports: [RouterModule.forRoot(routes, {useHash: true})], 13 | exports: [RouterModule] 14 | }) 15 | export class AppRoutingModule {} 16 | -------------------------------------------------------------------------------- /ui/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 12 |
13 | 14 | 15 | 16 | 17 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 | 36 |
37 | 38 |
39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /ui/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef} from '@angular/core'; 2 | import {SecurityService} from './security/service/security.service'; 3 | import {ModalService} from './shared/service/modal.service'; 4 | import {SettingsComponent} from './settings/settings/settings.component'; 5 | import {UrlUtilities} from './url-utilities.service'; 6 | 7 | @Component({ 8 | selector: 'app-root', 9 | templateUrl: './app.component.html' 10 | }) 11 | export class AppComponent { 12 | shouldProtect = this.securityService.shouldProtect(); 13 | securityEnabled = this.securityService.securityEnabled(); 14 | baseApiUrl = UrlUtilities.calculateBaseApiUrl(); 15 | 16 | constructor( 17 | private securityService: SecurityService, 18 | private modalService: ModalService, 19 | public elementRef: ElementRef 20 | ) {} 21 | 22 | openSettings(): void { 23 | this.modalService.show(SettingsComponent); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/apps/add/add.component.html: -------------------------------------------------------------------------------- 1 |

{{ 'applications.main.addApplications' | translate }}

2 | 3 | 4 | {{ 'applications.add.registerOneOrMore' | translate }} 5 | 6 | 7 | 8 | 9 | 10 | {{ 'applications.add.importFromHttp' | translate }} 11 | 12 | 13 | 14 | 15 | 16 | {{ 17 | 'applications.add.importFromFile' | translate 18 | }} 19 | 20 | 21 | 22 | 23 | 24 | {{ 'applications.add.importFromSpringDataFlow' | translate }}. 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ui/src/app/apps/add/add.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | p { 3 | padding: .6rem 0; 4 | margin: 0; 5 | } 6 | .clr-error .clr-select-wrapper:after, .clr-success .clr-select-wrapper:after { 7 | right: 0; 8 | } 9 | h1.title { 10 | margin-bottom: 1rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/app/apps/add/add.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-add', 5 | templateUrl: './add.component.html', 6 | styleUrls: ['./add.component.scss'] 7 | }) 8 | export class AddComponent { 9 | constructor() {} 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/app/apps/add/uri/uri.component.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | 5 | 6 | 16 | {{ 'applications.add.uri.eg' | translate }}: {{ 'applications.add.uri.egValue' | translate }} 18 | 19 | {{ 'applications.add.uri.invalidUri' | translate }} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 |
32 | 33 | 34 | 40 | 41 | -------------------------------------------------------------------------------- /ui/src/app/apps/apps-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | import {AppsComponent} from './apps.component'; 4 | import {AppComponent} from './app/app.component'; 5 | import {AddComponent} from './add/add.component'; 6 | import {SecurityGuard} from '../security/support/security.guard'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: 'apps', 11 | component: AppsComponent, 12 | canActivate: [SecurityGuard], 13 | data: { 14 | authenticate: true, 15 | roles: ['ROLE_VIEW'] 16 | } 17 | }, 18 | { 19 | path: 'apps/:appType/:appName', 20 | component: AppComponent, 21 | canActivate: [SecurityGuard], 22 | data: { 23 | authenticate: true, 24 | roles: ['ROLE_VIEW'] 25 | } 26 | }, 27 | { 28 | path: 'apps/add', 29 | component: AddComponent, 30 | data: { 31 | authenticate: true, 32 | roles: ['ROLE_CREATE'] 33 | } 34 | } 35 | ]; 36 | 37 | @NgModule({ 38 | imports: [RouterModule.forChild(routes)], 39 | exports: [RouterModule] 40 | }) 41 | export class AppsRoutingModule {} 42 | -------------------------------------------------------------------------------- /ui/src/app/apps/apps.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | 4 | import {ClarityModule} from '@clr/angular'; 5 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 6 | import {SharedModule} from '../shared/shared.module'; 7 | import {SecurityModule} from '../security/security.module'; 8 | import {AppsRoutingModule} from './apps-routing.module'; 9 | import {AddComponent} from './add/add.component'; 10 | import {AppComponent} from './app/app.component'; 11 | import {AppsComponent} from './apps.component'; 12 | import {UnregisterComponent} from './unregister/unregister.component'; 13 | import {VersionComponent} from './version/version.component'; 14 | import {TypeFilterComponent} from './type.filter'; 15 | import {PropsComponent} from './add/props/props.component'; 16 | import {RegisterComponent} from './add/register/register.component'; 17 | import {UriComponent} from './add/uri/uri.component'; 18 | import {WebsiteStartersComponent} from './add/website-starters/website-starters.component'; 19 | import {TranslateModule} from '@ngx-translate/core'; 20 | 21 | @NgModule({ 22 | declarations: [ 23 | AddComponent, 24 | AppComponent, 25 | AppsComponent, 26 | UnregisterComponent, 27 | VersionComponent, 28 | TypeFilterComponent, 29 | PropsComponent, 30 | RegisterComponent, 31 | UriComponent, 32 | WebsiteStartersComponent 33 | ], 34 | imports: [ 35 | CommonModule, 36 | FormsModule, 37 | ReactiveFormsModule, 38 | SharedModule, 39 | ClarityModule, 40 | SecurityModule, 41 | AppsRoutingModule, 42 | TranslateModule 43 | ], 44 | providers: [] 45 | }) 46 | export class AppsModule {} 47 | -------------------------------------------------------------------------------- /ui/src/app/dev/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .wizard .clr-textarea { 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/dev/dev-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {RouterModule, Routes} from '@angular/router'; 2 | import {NgModule} from '@angular/core'; 3 | import {DashboardComponent} from './dashboard/dashboard.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: 'dev', 8 | redirectTo: 'dev/dashboard' 9 | }, 10 | { 11 | path: 'dev/dashboard', 12 | component: DashboardComponent 13 | } 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forChild(routes)], 18 | exports: [RouterModule] 19 | }) 20 | export class DevRoutingModule {} 21 | -------------------------------------------------------------------------------- /ui/src/app/dev/dev.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {ClarityModule} from '@clr/angular'; 4 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 5 | import {SharedModule} from '../shared/shared.module'; 6 | import {DashboardComponent} from './dashboard/dashboard.component'; 7 | import {DevRoutingModule} from './dev-routing.module'; 8 | import {StreamCreateComponent} from './dashboard/stream-create/stream-create.component'; 9 | import {TaskCreateComponent} from './dashboard/task-create/task-create.component'; 10 | 11 | @NgModule({ 12 | declarations: [DashboardComponent, StreamCreateComponent, TaskCreateComponent], 13 | imports: [CommonModule, ClarityModule, FormsModule, ReactiveFormsModule, SharedModule, DevRoutingModule] 14 | }) 15 | export class DevModule {} 16 | -------------------------------------------------------------------------------- /ui/src/app/flo/shared-flo.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {FloModule} from 'spring-flo'; 3 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 4 | import {CommonModule} from '@angular/common'; 5 | import {HttpClientModule} from '@angular/common/http'; 6 | import {ClarityModule} from '@clr/angular'; 7 | import {ClrDynamicFormPropertyComponent} from './shared/properties/df.property.component'; 8 | import {ClrPropertiesGroupComponent} from './shared/properties/properties.group.component'; 9 | import {PropertiesDialogComponent} from './shared/properties/properties-dialog.component'; 10 | import {PropertiesGroupsDialogComponent} from './shared/properties-groups/properties-groups-dialog.component'; 11 | import {TranslateModule} from '@ngx-translate/core'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | ClrDynamicFormPropertyComponent, 16 | ClrPropertiesGroupComponent, 17 | PropertiesDialogComponent, 18 | PropertiesGroupsDialogComponent 19 | ], 20 | imports: [ 21 | CommonModule, 22 | HttpClientModule, 23 | FormsModule, 24 | ReactiveFormsModule, 25 | ClarityModule, 26 | TranslateModule, 27 | FloModule 28 | ], 29 | providers: [], 30 | exports: [ 31 | ClrDynamicFormPropertyComponent, 32 | ClrPropertiesGroupComponent, 33 | PropertiesDialogComponent, 34 | PropertiesGroupsDialogComponent 35 | ] 36 | }) 37 | export class SharedFloModule {} 38 | -------------------------------------------------------------------------------- /ui/src/app/flo/shared/graph-view/graph-view.component.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /ui/src/app/flo/shared/graph-view/graph-view.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewEncapsulation, Input, Output, EventEmitter} from '@angular/core'; 2 | import {Flo} from 'spring-flo'; 3 | 4 | @Component({ 5 | selector: 'app-graph-view', 6 | templateUrl: './graph-view.component.html', 7 | styleUrls: ['../flo.scss'], 8 | encapsulation: ViewEncapsulation.None 9 | }) 10 | export class GraphViewComponent { 11 | @Input() 12 | dsl: string; 13 | 14 | @Input() 15 | paperPadding = 5; 16 | 17 | @Input() 18 | metamodel: Flo.Metamodel; 19 | 20 | @Input() 21 | renderer: Flo.Renderer; 22 | 23 | @Output() 24 | floApi = new EventEmitter(); 25 | 26 | private editorContext: Flo.EditorContext; 27 | 28 | constructor() {} 29 | 30 | setEditorContext(editorContext: Flo.EditorContext): void { 31 | this.editorContext = editorContext; 32 | this.editorContext.noPalette = true; 33 | this.editorContext.readOnlyCanvas = true; 34 | this.floApi.emit(this.editorContext); 35 | } 36 | 37 | get flo(): Flo.EditorContext { 38 | return this.editorContext; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ui/src/app/flo/shared/properties/df.property.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit, ViewEncapsulation} from '@angular/core'; 2 | import {UntypedFormGroup, AbstractControl, FormControl} from '@angular/forms'; 3 | import {DynamicFormPropertyComponent, Properties} from 'spring-flo'; 4 | import PropertyFilter = Properties.PropertyFilter; 5 | 6 | @Component({ 7 | selector: 'clr-df-property', 8 | templateUrl: './df.property.component.html', 9 | encapsulation: ViewEncapsulation.None 10 | }) 11 | export class ClrDynamicFormPropertyComponent { 12 | @Input() 13 | model: Properties.ControlModel; 14 | 15 | @Input() form: UntypedFormGroup; 16 | 17 | constructor() {} 18 | 19 | get types(): any { 20 | return Properties.InputType; 21 | } 22 | 23 | get control(): AbstractControl { 24 | return this.form.controls[this.model.id]; 25 | } 26 | 27 | get errorData(): any { 28 | return (this.model.validation && this.model.validation.errorData ? this.model.validation.errorData : []).filter( 29 | e => this.control.errors && this.control.errors[e.id] 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/app/flo/shared/properties/properties.group.component.html: -------------------------------------------------------------------------------- 1 |
6 |
13 | {{ 'commons.noResultFound' | translate }} 14 |
15 |
16 | 17 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /ui/src/app/flo/shared/properties/properties.group.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit, ViewEncapsulation} from '@angular/core'; 2 | import {UntypedFormGroup, UntypedFormControl} from '@angular/forms'; 3 | import {Properties} from 'spring-flo'; 4 | import PropertyFilter = Properties.PropertyFilter; 5 | 6 | @Component({ 7 | selector: 'clr-properties-group', 8 | templateUrl: './properties.group.component.html', 9 | encapsulation: ViewEncapsulation.None 10 | }) 11 | export class ClrPropertiesGroupComponent implements OnInit { 12 | @Input() 13 | propertiesGroupModel: Properties.PropertiesGroupModel; 14 | 15 | @Input() 16 | form: UntypedFormGroup; 17 | 18 | @Input() 19 | filter: PropertyFilter; 20 | 21 | ngOnInit(): void { 22 | if (this.propertiesGroupModel.isLoading) { 23 | const subscription = this.propertiesGroupModel.loadedSubject.subscribe(loaded => { 24 | if (loaded) { 25 | subscription.unsubscribe(); 26 | this.createGroupControls(); 27 | } 28 | }); 29 | } else { 30 | this.createGroupControls(); 31 | } 32 | } 33 | 34 | createGroupControls(): void { 35 | this.propertiesGroupModel.getControlsModels().forEach(c => { 36 | if (c.validation) { 37 | this.form.addControl( 38 | c.id, 39 | new UntypedFormControl(c.value || '', c.validation.validator, c.validation.asyncValidator) 40 | ); 41 | } else { 42 | this.form.addControl(c.id, new UntypedFormControl(c.value || '')); 43 | } 44 | }); 45 | } 46 | 47 | get controlModelsToDisplay(): any { 48 | return this.propertiesGroupModel.getControlsModels().filter(c => !this.filter || this.filter.accept(c.property)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ui/src/app/flo/shared/service/doc.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable} from '@angular/core'; 2 | import {DOCUMENT} from '@angular/common'; 3 | 4 | @Injectable() 5 | export class DocService { 6 | private mouseDown = false; 7 | 8 | constructor(@Inject(DOCUMENT) private document: Document) { 9 | document.body.addEventListener('mouseup', () => (this.mouseDown = false)); 10 | document.body.addEventListener('mousedown', () => (this.mouseDown = true)); 11 | } 12 | 13 | body(): HTMLElement { 14 | return this.document.body; 15 | } 16 | 17 | isMouseDown(): boolean { 18 | return this.mouseDown; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/flo/shared/service/parser.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Parser} from './parser'; 3 | 4 | @Injectable() 5 | export class ParserService { 6 | constructor() {} 7 | 8 | parseDsl(text: string, mode?: string): any { 9 | return Parser.parse(text, mode); 10 | } 11 | 12 | simplifyDsl(dsl: string): string { 13 | return Parser.simplify(dsl); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/app/flo/shared/support/app-ui-property.ts: -------------------------------------------------------------------------------- 1 | import {Properties} from 'spring-flo'; 2 | 3 | /** 4 | * App property consumable by properties dialog 5 | * 6 | * @author Alex Boyko 7 | */ 8 | export interface AppUiProperty extends Properties.Property { 9 | attr: string; 10 | isSemantic: boolean; 11 | groups: string[]; 12 | code?: CodeProperty; 13 | } 14 | 15 | export interface CodeProperty { 16 | langPropertyName: string; 17 | language: string; 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/flo/shared/support/shape-component.ts: -------------------------------------------------------------------------------- 1 | import {dia} from 'jointjs'; 2 | 3 | export class ShapeComponent { 4 | public data: any; 5 | } 6 | 7 | export class ElementComponent { 8 | public view: dia.ElementView; 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/flo/stream/content-assist.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient, HttpParams} from '@angular/common/http'; 3 | import {Observable} from 'rxjs'; 4 | import {map} from 'rxjs/operators'; 5 | import {HttpUtils} from '../../shared/support/http.utils'; 6 | import {UrlUtilities} from '../../url-utilities.service'; 7 | 8 | @Injectable() 9 | export class ContentAssistService { 10 | constructor(private httpClient: HttpClient) {} 11 | 12 | getProposals(prefix: string): Observable> { 13 | const headers = HttpUtils.getDefaultHttpHeaders(); 14 | const params = new HttpParams().append('start', prefix).append('defaultLevel', '1'); 15 | return this.httpClient 16 | .get(UrlUtilities.calculateBaseApiUrl() + 'completions/stream', {headers, params}) 17 | .pipe(map(jsonResponse => jsonResponse.proposals)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/flo/stream/dsl-sanitize.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {TextToGraphConverter} from './text-to-graph'; 3 | import {Parser} from '../shared/service/parser'; 4 | 5 | /** 6 | * Helper to break down stream DSL text into stream definitions 7 | * 8 | * @author Alex Boyko 9 | */ 10 | @Injectable() 11 | export class SanitizeDsl { 12 | convert(dsl: string, parsedStreams: Parser.ParseResult): any { 13 | return TextToGraphConverter.convertParseResponseToJsonGraph(dsl, parsedStreams); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/app/flo/stream/instance-dot/instance-dot.component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 27 | 28 | {{ 'flo.commons.instanceDown' | translate }} 29 | 30 | -------------------------------------------------------------------------------- /ui/src/app/flo/stream/instance-dot/instance-dot.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared/flo'; 2 | 3 | .instance-dot:hover circle { 4 | stroke: $navy-6; 5 | } 6 | 7 | .joint-type-dataflow-instancedot.joint-element { 8 | .deployed { 9 | // stroke: $spring-green; 10 | stroke: #80b600; 11 | } 12 | .failed { 13 | // stroke: red; 14 | stroke: #ff5400; 15 | } 16 | stroke: gray; 17 | fill: transparent; 18 | cursor: initial; 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/flo/stream/instance-dot/instance-dot.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewEncapsulation} from '@angular/core'; 2 | import {ElementComponent} from '../../shared/support/shape-component'; 3 | import {InstanceStatus} from '../../../shared/model/metrics.model'; 4 | 5 | /** 6 | * Component for displaying "dot" for instance streamStatuses data under the module 7 | * 8 | * @author Alex Boyko 9 | * @author Andy Clement 10 | */ 11 | @Component({ 12 | selector: 'app-flo-instance-dot', 13 | templateUrl: 'instance-dot.component.html', 14 | styleUrls: ['instance-dot.component.scss'], 15 | encapsulation: ViewEncapsulation.None 16 | }) 17 | export class InstanceDotComponent extends ElementComponent { 18 | get instance(): InstanceStatus { 19 | return this.view ? this.view.model.attr('instance') : undefined; 20 | } 21 | 22 | get guid(): string { 23 | return this.instance ? this.instance.guid : ''; 24 | } 25 | 26 | get state(): string { 27 | return this.instance ? this.instance.state : ''; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/app/flo/stream/message-rate/message-rate.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ rateLabel }} 5 | 6 | 7 | 8 | 12 | 13 | {{ rateTooltip }} 14 | -------------------------------------------------------------------------------- /ui/src/app/flo/stream/message-rate/message-rate.component.scss: -------------------------------------------------------------------------------- 1 | .joint-link { 2 | .label.dataflow-outgoing-rate { 3 | text { 4 | fill: black; 5 | stroke: none; 6 | font-size: 8px; 7 | } 8 | rect { 9 | stroke: black; 10 | stroke-width: 1px; 11 | fill: #CFE2F3; 12 | } 13 | } 14 | .label.dataflow-incoming-rate { 15 | text { 16 | fill: white; 17 | stroke: none; 18 | font-size: 8px; 19 | } 20 | rect { 21 | stroke: #3498DB; 22 | fill: #3498DB; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/flo/stream/node/stream-node.component.scss: -------------------------------------------------------------------------------- 1 | .app-tooltip .tooltip-inner { 2 | max-width: 400px; 3 | } 4 | 5 | .tooltip-property-value { 6 | word-break: break-all; 7 | max-width: 390px; 8 | white-space: pre; 9 | text-overflow: ellipsis; 10 | overflow: hidden; 11 | text-align: left; 12 | } 13 | 14 | .tooltip-property-key { 15 | text-align: left; 16 | vertical-align: top; 17 | max-width: 200px; 18 | vertical-align: top; 19 | text-overflow: ellipsis; 20 | overflow: hidden; 21 | } 22 | 23 | .tooltip-property-value-code { 24 | font-family: monospace; 25 | font-size: 85%; 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/flo/stream/node/stream-node.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef, ViewChild, ViewEncapsulation} from '@angular/core'; 2 | import {dia} from 'jointjs'; 3 | import {NodeComponent} from '../../shared/support/node-component'; 4 | import {DocService} from '../../shared/service/doc.service'; 5 | import {PropertiesEditor} from '../properties-editor.service'; 6 | import {UrlUtilities} from '../../../url-utilities.service'; 7 | 8 | /** 9 | * Component for displaying application properties and capturing their values. 10 | * 11 | * @author Alex Boyko 12 | * @author Andy Clement 13 | */ 14 | @Component({ 15 | selector: 'app-flo-node', 16 | templateUrl: './stream-node.component.html', 17 | styleUrls: ['./stream-node.component.scss'], 18 | encapsulation: ViewEncapsulation.None 19 | }) 20 | export class StreamNodeComponent extends NodeComponent { 21 | @ViewChild('markerTooltip') 22 | markerTooltipElement: ElementRef; 23 | 24 | @ViewChild('canvasNodeTooltip') 25 | canvasTooltipElement: ElementRef; 26 | 27 | @ViewChild('paletteNodeTooltip') 28 | paletteTooltipElement: ElementRef; 29 | 30 | assetUrl = UrlUtilities.calculateAssetUrl(); 31 | 32 | constructor( 33 | docService: DocService, 34 | private propertiesEditor: PropertiesEditor 35 | ) { 36 | super(docService); 37 | } 38 | 39 | showOptions(): void { 40 | this.propertiesEditor.showForNode(this.view.model, this.paper.model); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/app/flo/stream/support/layout.ts: -------------------------------------------------------------------------------- 1 | import {dia} from 'jointjs'; 2 | import * as dagre from 'dagre'; 3 | import {Flo} from 'spring-flo'; 4 | 5 | const NODE_SEPARATION = 60.0; 6 | const RANK_SEPARATION = 60.0; 7 | const EDGE_SEPARATION = 30.0; 8 | 9 | export function layout(paper: dia.Paper): void { 10 | const graph = paper.model; 11 | 12 | const g = new dagre.graphlib.Graph(); 13 | g.setGraph({}); 14 | g.setDefaultEdgeLabel(() => ({})); 15 | 16 | graph 17 | .getElements() 18 | .filter(e => !e.get('parent')) 19 | .forEach(node => g.setNode(node.id, node.get('size'))); 20 | 21 | graph 22 | .getLinks() 23 | .filter(link => link.get('source').id && link.get('target').id) 24 | .forEach(link => { 25 | g.setEdge(link.get('source').id, link.get('target').id, {weight: link.get('source').port === 'output' ? 200 : 1}); 26 | link.set('vertices', []); 27 | }); 28 | 29 | const gridSize = paper.options.gridSize; 30 | g.graph().rankdir = 'LR'; 31 | g.graph().nodesep = gridSize <= 1 ? NODE_SEPARATION : Math.round(NODE_SEPARATION / gridSize) * gridSize; 32 | g.graph().ranksep = gridSize <= 1 ? RANK_SEPARATION : Math.round(RANK_SEPARATION / gridSize) * gridSize; 33 | g.graph().edgesep = gridSize <= 1 ? EDGE_SEPARATION : Math.round(EDGE_SEPARATION / gridSize) * gridSize; 34 | 35 | dagre.layout(g); 36 | g.nodes().forEach(v => { 37 | const node = graph.getCell(v) as dia.Element; 38 | if (node) { 39 | const bbox = node.getBBox(); 40 | node.translate(g.node(v).x - g.node(v).width / 2 - bbox.x, g.node(v).y - g.node(v).height / 2 - bbox.y); 41 | } 42 | }); 43 | 44 | g.edges().forEach(o => g.edge(o)); 45 | } 46 | 47 | export function arrangeAll(editorContext: Flo.EditorContext): Promise { 48 | return editorContext.performLayout().then(() => { 49 | editorContext.fitToPage(); 50 | const currentScale = editorContext.getPaper().scale(); 51 | if (currentScale.sx > 1 || currentScale.sy > 1) { 52 | editorContext.getPaper().scale(1); 53 | } 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /ui/src/app/flo/task-flo.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {HttpClientModule} from '@angular/common/http'; 4 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 5 | import {ClarityModule} from '@clr/angular'; 6 | import {FloModule} from 'spring-flo'; 7 | import {DocService} from './shared/service/doc.service'; 8 | import {EditorService} from './task/editor.service'; 9 | import {MetamodelService} from './task/metamodel.service'; 10 | import {RenderService} from './task/render.service'; 11 | import {ParserService} from './shared/service/parser.service'; 12 | import {ContentAssistService} from './task/content-assist.service'; 13 | import {TaskNodeComponent} from './task/node/task-node.component'; 14 | import {TaskPropertiesDialogComponent} from './task/properties/task-properties-dialog-component'; 15 | import {ViewComponent} from './task/component/view.component'; 16 | import {TaskFloCreateComponent} from './task/component/create.component'; 17 | import {ToolsService} from './task/tools.service'; 18 | import {SharedFloModule} from './shared-flo.module'; 19 | import {SharedModule} from '../shared/shared.module'; 20 | import {TranslateModule} from '@ngx-translate/core'; 21 | 22 | @NgModule({ 23 | declarations: [TaskNodeComponent, TaskPropertiesDialogComponent, ViewComponent, TaskFloCreateComponent], 24 | imports: [ 25 | CommonModule, 26 | HttpClientModule, 27 | FormsModule, 28 | ReactiveFormsModule, 29 | ClarityModule, 30 | FloModule, 31 | SharedFloModule, 32 | TranslateModule, 33 | SharedModule 34 | ], 35 | providers: [ 36 | DocService, 37 | EditorService, 38 | MetamodelService, 39 | RenderService, 40 | ParserService, 41 | ContentAssistService, 42 | ToolsService 43 | ], 44 | exports: [ViewComponent, TaskFloCreateComponent, TaskPropertiesDialogComponent] 45 | }) 46 | export class TaskFloModule {} 47 | -------------------------------------------------------------------------------- /ui/src/app/flo/task/component/view.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | import {Flo} from 'spring-flo'; 3 | import {MetamodelService} from '../metamodel.service'; 4 | import {RenderService} from '../render.service'; 5 | 6 | @Component({ 7 | selector: 'app-task-flo-view', 8 | template: ` 9 |
10 | 17 | 18 |
19 | `, 20 | styleUrls: ['../../shared/flo.scss'] 21 | }) 22 | export class ViewComponent { 23 | @Input() 24 | dsl: string; 25 | 26 | @Input() 27 | paperPadding = 5; 28 | 29 | private editorContext: Flo.EditorContext; 30 | 31 | constructor( 32 | public metamodelService: MetamodelService, 33 | public renderService: RenderService 34 | ) {} 35 | 36 | setEditorContext(editorContext: Flo.EditorContext): void { 37 | this.editorContext = editorContext; 38 | this.editorContext.noPalette = true; 39 | this.editorContext.readOnlyCanvas = true; 40 | } 41 | 42 | get flo(): Flo.EditorContext { 43 | return this.editorContext; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ui/src/app/flo/task/content-assist.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient, HttpParams} from '@angular/common/http'; 3 | import {Observable} from 'rxjs'; 4 | import {map} from 'rxjs/operators'; 5 | import {HttpUtils} from '../../shared/support/http.utils'; 6 | import {UrlUtilities} from '../../url-utilities.service'; 7 | 8 | @Injectable() 9 | export class ContentAssistService { 10 | constructor(private httpClient: HttpClient) {} 11 | 12 | getProposals(prefix: string): Observable> { 13 | const headers = HttpUtils.getDefaultHttpHeaders(); 14 | const params = new HttpParams().append('start', prefix).append('defaultLevel', '1'); 15 | return this.httpClient 16 | .get(UrlUtilities.calculateBaseApiUrl() + 'completions/task', {headers, params}) 17 | .pipe(map(jsonResponse => jsonResponse.proposals)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/flo/task/model/models.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic response type returned from both tools conversions. 3 | */ 4 | export class TaskConversion { 5 | public graph: Graph; 6 | public dsl: string; 7 | public errors: Array = []; 8 | 9 | constructor(dsl: string, errors?: Array>, graph?: Graph) { 10 | this.dsl = dsl; 11 | if (errors) { 12 | this.errors = errors; 13 | } 14 | this.graph = graph; 15 | } 16 | } 17 | 18 | /** 19 | * Represents a graph in a TaskConversion response. 20 | */ 21 | export class Graph { 22 | public nodes: Array; 23 | public links: Array; 24 | 25 | constructor(nodes: Array, links: Array) { 26 | this.nodes = nodes; 27 | this.links = links; 28 | } 29 | 30 | toJson(): string { 31 | return JSON.stringify(this); 32 | } 33 | } 34 | 35 | /** 36 | * Represents node in a Graph. 37 | */ 38 | export class Node { 39 | public id: string; 40 | public name: string; 41 | public properties: any; 42 | public metadata: any; 43 | 44 | constructor(id: string, name: string, properties?: any, metadata?: any) { 45 | this.id = id; 46 | this.name = name; 47 | this.properties = properties; 48 | this.metadata = metadata; 49 | } 50 | } 51 | 52 | /** 53 | * Represents link in a Graph. 54 | */ 55 | export class Link { 56 | public from: string; 57 | public to: string; 58 | public properties: any; 59 | 60 | constructor(from: string, to: string, properties?: any) { 61 | this.from = from; 62 | this.to = to; 63 | this.properties = properties; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ui/src/app/flo/task/node/task-node.component.scss: -------------------------------------------------------------------------------- 1 | .app-tooltip .tooltip-inner { 2 | max-width: 400px; 3 | } 4 | 5 | .tooltip-property-value { 6 | word-break: break-all; 7 | } 8 | 9 | .tooltip-property-key { 10 | vertical-align: top; 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/app/flo/task/node/task-node.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef, ViewChild, ViewEncapsulation} from '@angular/core'; 2 | import {TaskGraphPropertiesSource} from '../properties/task-properties-source'; 3 | import {NodeComponent} from '../../shared/support/node-component'; 4 | import {DocService} from '../../shared/service/doc.service'; 5 | import {ModalService} from '../../../shared/service/modal.service'; 6 | import {App, ApplicationType} from '../../../shared/model/app.model'; 7 | import {TaskPropertiesDialogComponent} from '../properties/task-properties-dialog-component'; 8 | import {UrlUtilities} from '../../../url-utilities.service'; 9 | 10 | /** 11 | * Component for displaying application properties and capturing their values. 12 | * 13 | * @author Alex Boyko 14 | * @author Andy Clement 15 | */ 16 | @Component({ 17 | selector: 'task-flo-node', 18 | templateUrl: 'task-node.component.html', 19 | styleUrls: ['./task-node.component.scss'], 20 | encapsulation: ViewEncapsulation.None 21 | }) 22 | export class TaskNodeComponent extends NodeComponent { 23 | @ViewChild('markerTooltip') 24 | markerTooltipElement: ElementRef; 25 | 26 | @ViewChild('nodeTooltip') 27 | nodeTooltipElement: ElementRef; 28 | 29 | assetUrl = UrlUtilities.calculateAssetUrl(); 30 | 31 | constructor( 32 | docService: DocService, 33 | private modalService?: ModalService 34 | ) { 35 | super(docService); 36 | } 37 | 38 | showOptions(): void { 39 | const element = this.view.model; 40 | const modal = this.modalService.show(TaskPropertiesDialogComponent); 41 | const app = new App(); 42 | app.name = element.prop('metadata/name'); 43 | app.type = ApplicationType.task; 44 | modal.app = app; 45 | modal.title = `Properties for ${element.prop('metadata/name').toUpperCase()}`; 46 | modal.setData(new TaskGraphPropertiesSource(element)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ui/src/app/flo/task/properties/task-properties-source.ts: -------------------------------------------------------------------------------- 1 | import {Flo} from 'spring-flo'; 2 | import {dia} from 'jointjs'; 3 | import {GraphNodePropertiesSource} from '../../shared/support/graph-node-properties-source'; 4 | import {AppUiProperty} from '../../shared/support/app-ui-property'; 5 | import {ApplicationType} from '../../../shared/model/app.model'; 6 | 7 | /** 8 | * Properties source for Composed Tasks graph node 9 | * 10 | * @author Alex Boyko 11 | */ 12 | export class TaskGraphPropertiesSource extends GraphNodePropertiesSource { 13 | protected createNotationalProperties(): Array { 14 | const notationalProperties = []; 15 | if (typeof ApplicationType[this.cell.prop('metadata/group')] === 'number') { 16 | notationalProperties.push({ 17 | id: 'label', 18 | name: 'label', 19 | defaultValue: this.cell.prop('metadata/name'), 20 | attr: 'node-label', 21 | value: this.cell.attr('node-label'), 22 | description: 'Label of the task', 23 | isSemantic: false 24 | }); 25 | } 26 | return notationalProperties; 27 | } 28 | 29 | protected determineAttributeName(metadata: Flo.PropertyMetadata): string { 30 | if (this.cell instanceof dia.Link) { 31 | // For links properties are always id based 32 | return `props/${metadata.id}`; 33 | } 34 | return super.determineAttributeName(metadata); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ui/src/app/layout/layout.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {RouterModule} from '@angular/router'; 4 | import {BrowserModule} from '@angular/platform-browser'; 5 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 6 | import {ClarityModule} from '@clr/angular'; 7 | import {NavComponent} from './nav/nav.component'; 8 | import {LogoComponent} from './logo/logo.component'; 9 | import {SecurityModule} from '../security/security.module'; 10 | import {TranslateModule} from '@ngx-translate/core'; 11 | 12 | @NgModule({ 13 | declarations: [LogoComponent, NavComponent], 14 | imports: [ 15 | RouterModule, 16 | BrowserModule, 17 | BrowserAnimationsModule, 18 | CommonModule, 19 | ClarityModule, 20 | SecurityModule, 21 | TranslateModule 22 | ], 23 | exports: [LogoComponent, NavComponent] 24 | }) 25 | export class LayoutModule {} 26 | -------------------------------------------------------------------------------- /ui/src/app/layout/nav/nav.component.scss: -------------------------------------------------------------------------------- 1 | // Needed for left navi bar to span all the way down 2 | :host { 3 | display: flex; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/layout/nav/nav.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {SecurityService} from '../../security/service/security.service'; 3 | import {environment} from '../../../environments/environment'; 4 | 5 | @Component({ 6 | selector: 'app-nav', 7 | templateUrl: './nav.component.html', 8 | styleUrls: ['./nav.component.scss'] 9 | }) 10 | export class NavComponent { 11 | shouldProtect = this.securityService.shouldProtect(); 12 | 13 | constructor(private securityService: SecurityService) {} 14 | } 15 | -------------------------------------------------------------------------------- /ui/src/app/layout/theme/default-theme.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from './types'; 2 | 3 | export const defaultTheme: Theme = { 4 | name: 'default', 5 | properties: { 6 | '--clr-datagrid-row-hover': 'hsl(198, 36%, 96%)' 7 | }, 8 | other: '' 9 | }; 10 | -------------------------------------------------------------------------------- /ui/src/app/layout/theme/types.ts: -------------------------------------------------------------------------------- 1 | export interface Theme { 2 | name: string; 3 | properties: any; 4 | other: string; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/manage/manage-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | import {ToolsComponent} from './tools/tools.component'; 4 | import {RecordsComponent} from './records/records.component'; 5 | import {SecurityGuard} from '../security/support/security.guard'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: 'manage', 10 | canActivate: [SecurityGuard], 11 | data: { 12 | authenticate: true, 13 | roles: ['ROLE_VIEW'] 14 | }, 15 | children: [ 16 | { 17 | path: 'tools', 18 | component: ToolsComponent, 19 | data: { 20 | authenticate: true, 21 | roles: ['ROLE_CREATE'] 22 | } 23 | }, 24 | { 25 | path: 'records', 26 | component: RecordsComponent 27 | } 28 | ] 29 | } 30 | ]; 31 | 32 | @NgModule({ 33 | imports: [RouterModule.forChild(routes)], 34 | exports: [RouterModule] 35 | }) 36 | export class ManageRoutingModule {} 37 | -------------------------------------------------------------------------------- /ui/src/app/manage/manage.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | 4 | import {ManageRoutingModule} from './manage-routing.module'; 5 | import {RecordsComponent} from './records/records.component'; 6 | import {ToolsComponent} from './tools/tools.component'; 7 | import {ClarityModule} from '@clr/angular'; 8 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 9 | import {SharedModule} from '../shared/shared.module'; 10 | import {StreamImportComponent} from './tools/stream/import.component'; 11 | import {StreamExportComponent} from './tools/stream/export.component'; 12 | import {TaskExportComponent} from './tools/task/export.component'; 13 | import {TaskImportComponent} from './tools/task/import.component'; 14 | import {SecurityModule} from '../security/security.module'; 15 | import {OperationFilterComponent} from './records/operation.filter'; 16 | import {ActionFilterComponent} from './records/action.filter'; 17 | import {CleanupComponent} from './tools/cleanup/cleanup.component'; 18 | import {TranslateModule} from '@ngx-translate/core'; 19 | 20 | @NgModule({ 21 | declarations: [ 22 | RecordsComponent, 23 | ToolsComponent, 24 | StreamImportComponent, 25 | StreamExportComponent, 26 | TaskExportComponent, 27 | TaskImportComponent, 28 | OperationFilterComponent, 29 | ActionFilterComponent, 30 | CleanupComponent 31 | ], 32 | imports: [ 33 | CommonModule, 34 | ManageRoutingModule, 35 | FormsModule, 36 | ReactiveFormsModule, 37 | SharedModule, 38 | ClarityModule, 39 | SecurityModule, 40 | TranslateModule 41 | ], 42 | providers: [] 43 | }) 44 | export class ManageModule {} 45 | -------------------------------------------------------------------------------- /ui/src/app/manage/tools/tools.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewChild} from '@angular/core'; 2 | import {NotificationService} from '../../shared/service/notification.service'; 3 | import {CleanupComponent} from './cleanup/cleanup.component'; 4 | import {StreamExportComponent} from './stream/export.component'; 5 | import {StreamImportComponent} from './stream/import.component'; 6 | import {TaskExportComponent} from './task/export.component'; 7 | import {TaskImportComponent} from './task/import.component'; 8 | import {UrlUtilities} from '../../url-utilities.service'; 9 | 10 | @Component({ 11 | selector: 'app-tools', 12 | templateUrl: './tools.component.html' 13 | }) 14 | export class ToolsComponent { 15 | @ViewChild('streamImportModal', {static: true}) 16 | streamImportModal: StreamImportComponent; 17 | @ViewChild('streamExportModal', {static: true}) 18 | streamExportModal: StreamExportComponent; 19 | @ViewChild('taskExportModal', {static: true}) 20 | taskExportModal: TaskExportComponent; 21 | @ViewChild('taskImportModal', {static: true}) 22 | taskImportModal: TaskImportComponent; 23 | @ViewChild('cleanupModal', {static: true}) 24 | cleanupModal: CleanupComponent; 25 | baseApiUrl = UrlUtilities.calculateBaseApiUrl(); 26 | days: number; 27 | 28 | constructor(private notificationService: NotificationService) {} 29 | 30 | run(type: string): any { 31 | switch (type) { 32 | case 'import-stream': 33 | this.streamImportModal.open(); 34 | break; 35 | case 'export-stream': 36 | this.streamExportModal.open(); 37 | break; 38 | case 'export-task': 39 | this.taskExportModal.open(); 40 | break; 41 | case 'import-task': 42 | this.taskImportModal.open(); 43 | break; 44 | case 'cleanup-all': 45 | this.cleanupModal.open('all', this.days); 46 | break; 47 | case 'cleanup-completed': 48 | this.cleanupModal.open('completed', this.days); 49 | break; 50 | } 51 | return false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ui/src/app/reducers/reducer.ts: -------------------------------------------------------------------------------- 1 | import {InjectionToken} from '@angular/core'; 2 | import {ActionReducer, MetaReducer, Action, ActionReducerMap} from '@ngrx/store'; 3 | import * as fromRouter from '@ngrx/router-store'; 4 | import {environment} from '../../environments/environment'; 5 | 6 | export interface State { 7 | router: fromRouter.RouterReducerState; 8 | } 9 | 10 | export const ROOT_REDUCERS = new InjectionToken>('Root reducers token', { 11 | factory: () => ({ 12 | router: fromRouter.routerReducer 13 | }) 14 | }); 15 | 16 | export function logger(reducer: ActionReducer): ActionReducer { 17 | return (state, action) => { 18 | const result = reducer(state, action); 19 | console.groupCollapsed(action.type); 20 | console.log('prev state', state); 21 | console.log('action', action); 22 | console.log('next state', result); 23 | console.groupEnd(); 24 | return result; 25 | }; 26 | } 27 | 28 | export const metaReducers: MetaReducer[] = !environment.production ? [logger] : []; 29 | -------------------------------------------------------------------------------- /ui/src/app/security/component/authentication-required.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 5 |

{{ 'security.authenticationRequired.title' | translate }}

6 |

{{ 'security.authenticationRequired.description' | translate }}

7 | ` 8 | }) 9 | export class AuthenticationRequiredComponent {} 10 | -------------------------------------------------------------------------------- /ui/src/app/security/component/feature-disabled.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; 2 | import {FormsModule} from '@angular/forms'; 3 | import {ClarityModule} from '@clr/angular'; 4 | import {RouterTestingModule} from '@angular/router/testing'; 5 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 6 | import {SecurityServiceMock} from '../../tests/api/security.service.mock'; 7 | import {AboutServiceMock} from '../../tests/api/about.service.mock'; 8 | import {AppServiceMock} from '../../tests/api/app.service.mock'; 9 | import {NotificationServiceMock} from '../../tests/service/notification.service.mock'; 10 | import {FeatureDisabledComponent} from './feature-disabled.component'; 11 | import {TranslateTestingModule} from 'ngx-translate-testing'; 12 | import TRANSLATIONS from '../../../assets/i18n/en.json'; 13 | 14 | describe('security/component/feature-disabled.component.ts', () => { 15 | let component: FeatureDisabledComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach(waitForAsync(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [FeatureDisabledComponent], 21 | imports: [ 22 | FormsModule, 23 | ClarityModule, 24 | TranslateTestingModule.withTranslations('en', TRANSLATIONS), 25 | RouterTestingModule.withRoutes([]), 26 | BrowserAnimationsModule 27 | ], 28 | providers: [ 29 | SecurityServiceMock.provider, 30 | AboutServiceMock.provider, 31 | AppServiceMock.provider, 32 | NotificationServiceMock.provider 33 | ] 34 | }).compileComponents(); 35 | })); 36 | 37 | beforeEach(() => { 38 | NotificationServiceMock.mock.clearAll(); 39 | fixture = TestBed.createComponent(FeatureDisabledComponent); 40 | component = fixture.componentInstance; 41 | }); 42 | 43 | it('should be created', () => { 44 | fixture.detectChanges(); 45 | expect(component).toBeTruthy(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /ui/src/app/security/component/feature-disabled.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 5 |

{{ 'security.featureDisabled.title' | translate }}

6 |

{{ 'security.featureDisabled.description' | translate }}

7 | ` 8 | }) 9 | export class FeatureDisabledComponent {} 10 | -------------------------------------------------------------------------------- /ui/src/app/security/component/roles-missing.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; 2 | import {FormsModule} from '@angular/forms'; 3 | import {ClarityModule} from '@clr/angular'; 4 | import {RouterTestingModule} from '@angular/router/testing'; 5 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 6 | import {SecurityServiceMock} from '../../tests/api/security.service.mock'; 7 | import {AboutServiceMock} from '../../tests/api/about.service.mock'; 8 | import {AppServiceMock} from '../../tests/api/app.service.mock'; 9 | import {NotificationServiceMock} from '../../tests/service/notification.service.mock'; 10 | import {RolesMissingComponent} from './roles-missing.component'; 11 | import {TranslateTestingModule} from 'ngx-translate-testing'; 12 | import TRANSLATIONS from '../../../assets/i18n/en.json'; 13 | 14 | describe('security/component/roles-missing.component.ts', () => { 15 | let component: RolesMissingComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach(waitForAsync(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [RolesMissingComponent], 21 | imports: [ 22 | FormsModule, 23 | ClarityModule, 24 | TranslateTestingModule.withTranslations('en', TRANSLATIONS), 25 | RouterTestingModule.withRoutes([]), 26 | BrowserAnimationsModule 27 | ], 28 | providers: [ 29 | SecurityServiceMock.provider, 30 | AboutServiceMock.provider, 31 | AppServiceMock.provider, 32 | NotificationServiceMock.provider 33 | ] 34 | }).compileComponents(); 35 | })); 36 | 37 | beforeEach(() => { 38 | NotificationServiceMock.mock.clearAll(); 39 | fixture = TestBed.createComponent(RolesMissingComponent); 40 | component = fixture.componentInstance; 41 | }); 42 | 43 | it('should be created', () => { 44 | fixture.detectChanges(); 45 | expect(component).toBeTruthy(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /ui/src/app/security/component/roles-missing.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 5 |

{{ 'security.rolesMissing.title' | translate }}

6 |

{{ 'security.rolesMissing.description' | translate }}

7 | ` 8 | }) 9 | export class RolesMissingComponent {} 10 | -------------------------------------------------------------------------------- /ui/src/app/security/directive/role.directive.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, Directive, ElementRef, Input, Renderer2} from '@angular/core'; 2 | import get from 'lodash.get'; 3 | import {AboutService} from '../../shared/api/about.service'; 4 | import {SecurityService} from '../service/security.service'; 5 | 6 | @Directive({ 7 | selector: '[appRole]' 8 | }) 9 | export class RoleDirective implements AfterViewInit { 10 | @Input() 11 | public grantedRole: string[]; 12 | 13 | @Input() 14 | public appRole: string[]; 15 | 16 | @Input() 17 | public appFeature: string; 18 | 19 | private existingDisplayPropertyValue: string; 20 | 21 | constructor( 22 | private aboutService: AboutService, 23 | private securityService: SecurityService, 24 | private elem: ElementRef, 25 | private renderer: Renderer2 26 | ) {} 27 | 28 | private async checkRoles() { 29 | if (this.appFeature) { 30 | const features = this.appFeature.split(','); 31 | if (this.appFeature) { 32 | const result = await Promise.all([...features.map(feature => this.aboutService.isFeatureEnabled(feature))]); 33 | const featureEnabled = result.filter(item => item === true).length > 0; 34 | this.checkRoleAccess(featureEnabled); 35 | } 36 | } else { 37 | this.checkRoleAccess(true); 38 | } 39 | } 40 | 41 | private async checkRoleAccess(featureEnabled: boolean) { 42 | const hasRoleAccess = await this.securityService.canAccess(this.appRole); 43 | if (!featureEnabled || !hasRoleAccess) { 44 | this.renderer.setStyle(this.elem.nativeElement, 'display', 'none'); 45 | } else { 46 | if (this.existingDisplayPropertyValue) { 47 | this.renderer.setStyle(this.elem.nativeElement, 'display', this.existingDisplayPropertyValue); 48 | } else { 49 | this.renderer.removeStyle(this.elem.nativeElement, 'display'); 50 | } 51 | } 52 | } 53 | 54 | ngAfterViewInit(): void { 55 | this.existingDisplayPropertyValue = this.elem.nativeElement.style.display; 56 | this.checkRoles(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ui/src/app/security/security-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | import {FeatureDisabledComponent} from './component/feature-disabled.component'; 4 | import {RolesMissingComponent} from './component/roles-missing.component'; 5 | import {AuthenticationRequiredComponent} from './component/authentication-required.component'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: 'feature-disabled', 10 | component: FeatureDisabledComponent 11 | }, 12 | { 13 | path: 'roles-missing', 14 | component: RolesMissingComponent 15 | }, 16 | { 17 | path: 'authentication-required', 18 | component: AuthenticationRequiredComponent 19 | } 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forChild(routes)], 24 | exports: [RouterModule] 25 | }) 26 | export class SecurityRoutingModule {} 27 | -------------------------------------------------------------------------------- /ui/src/app/security/security.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {StoreModule} from '@ngrx/store'; 4 | import {EffectsModule} from '@ngrx/effects'; 5 | import {RoleDirective} from './directive/role.directive'; 6 | import {ClarityModule} from '@clr/angular'; 7 | import {SharedModule} from '../shared/shared.module'; 8 | import {RolesMissingComponent} from './component/roles-missing.component'; 9 | import {FeatureDisabledComponent} from './component/feature-disabled.component'; 10 | import {AuthenticationRequiredComponent} from './component/authentication-required.component'; 11 | import {SecurityRoutingModule} from './security-routing.module'; 12 | import {SecurityGuard} from './support/security.guard'; 13 | import {HTTP_INTERCEPTORS} from '@angular/common/http'; 14 | import * as fromSecurity from './store/security.reducer'; 15 | import {SecurityEffect} from './store/security.effect'; 16 | import {SecurityInterceptor} from './support/security.interceptor'; 17 | import {TranslateModule} from '@ngx-translate/core'; 18 | 19 | @NgModule({ 20 | declarations: [RoleDirective, RolesMissingComponent, FeatureDisabledComponent, AuthenticationRequiredComponent], 21 | imports: [ 22 | CommonModule, 23 | ClarityModule, 24 | SharedModule, 25 | SecurityRoutingModule, 26 | TranslateModule, 27 | StoreModule.forFeature(fromSecurity.securityFeatureKey, fromSecurity.reducer), 28 | EffectsModule.forFeature([SecurityEffect]) 29 | ], 30 | providers: [ 31 | SecurityGuard, 32 | { 33 | provide: HTTP_INTERCEPTORS, 34 | useClass: SecurityInterceptor, 35 | multi: true 36 | } 37 | ], 38 | exports: [RoleDirective] 39 | }) 40 | export class SecurityModule {} 41 | -------------------------------------------------------------------------------- /ui/src/app/security/store/security.action.ts: -------------------------------------------------------------------------------- 1 | import {createAction, props} from '@ngrx/store'; 2 | 3 | export const loaded = createAction( 4 | '[Security] Loaded', 5 | props<{enabled: boolean; authenticated: boolean; username: string; roles: string[]}>() 6 | ); 7 | export const logout = createAction('[Security] Logout'); 8 | export const unauthorised = createAction('[Security] Unauthorised'); 9 | -------------------------------------------------------------------------------- /ui/src/app/security/store/security.effect.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed, waitForAsync} from '@angular/core/testing'; 2 | import {provideMockActions} from '@ngrx/effects/testing'; 3 | import {cold} from 'jasmine-marbles'; 4 | import {Router} from '@angular/router'; 5 | import {Action} from '@ngrx/store'; 6 | import {Observable, of} from 'rxjs'; 7 | import * as SecurityAction from './security.action'; 8 | import {SecurityEffect} from './security.effect'; 9 | 10 | describe('Security Effect', () => { 11 | let effects: SecurityEffect; 12 | let actions$: Observable; 13 | const routerSpy = jasmine.createSpyObj('Router', ['navigate']); 14 | 15 | beforeEach(waitForAsync(() => { 16 | TestBed.configureTestingModule({ 17 | providers: [SecurityEffect, provideMockActions(() => actions$), {provide: Router, useValue: routerSpy}] 18 | }).compileComponents(); 19 | effects = TestBed.inject(SecurityEffect); 20 | })); 21 | 22 | it('Unauthorised should logout', () => { 23 | actions$ = of(SecurityAction.unauthorised()); 24 | const expected = cold('(a|)', {a: SecurityAction.logout()}); 25 | expect(effects.securityReset$).toBeObservable(expected); 26 | expect(routerSpy.navigate).toHaveBeenCalledWith(['/']); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /ui/src/app/security/store/security.effect.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | import {Actions, createEffect, ofType} from '@ngrx/effects'; 4 | import {tap, map} from 'rxjs/operators'; 5 | import * as SecurityActions from './security.action'; 6 | 7 | @Injectable() 8 | export class SecurityEffect { 9 | securityReset$ = createEffect( 10 | () => 11 | this.actions$.pipe( 12 | ofType(SecurityActions.unauthorised), 13 | map(SecurityActions.logout), 14 | tap(() => { 15 | this.router.navigate(['/']); 16 | }) 17 | ), 18 | {dispatch: true} 19 | ); 20 | 21 | constructor( 22 | private actions$: Actions, 23 | private router: Router 24 | ) {} 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/security/store/security.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import {createAction} from '@ngrx/store'; 2 | import * as fromSecurity from './security.reducer'; 3 | import * as SecurityActions from './security.action'; 4 | 5 | describe('security/store/security.reducer.ts', () => { 6 | it('should return init state', () => { 7 | const newState = fromSecurity.reducer(undefined, createAction('noop')); 8 | expect(newState).toEqual(fromSecurity.initialState); 9 | }); 10 | 11 | it('should load and logout', () => { 12 | let expectedState: fromSecurity.SecurityState = { 13 | enabled: false, 14 | authenticated: true, 15 | username: 'fakeuser', 16 | roles: ['role1', 'role2'] 17 | }; 18 | let newState = fromSecurity.reducer( 19 | undefined, 20 | SecurityActions.loaded({ 21 | enabled: false, 22 | authenticated: true, 23 | username: 'fakeuser', 24 | roles: ['role1', 'role2'] 25 | }) 26 | ); 27 | expect(newState).toEqual(expectedState); 28 | expectedState = { 29 | enabled: false, 30 | authenticated: false, 31 | username: undefined, 32 | roles: [] 33 | }; 34 | newState = fromSecurity.reducer(newState, SecurityActions.logout()); 35 | expect(newState).toEqual(expectedState); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /ui/src/app/security/store/security.reducer.ts: -------------------------------------------------------------------------------- 1 | import {createReducer, createSelector, on} from '@ngrx/store'; 2 | import * as SecurityActions from './security.action'; 3 | import * as fromRoot from '../../reducers/reducer'; 4 | 5 | export const securityFeatureKey = 'security'; 6 | 7 | export interface SecurityState { 8 | enabled: boolean; 9 | authenticated: boolean; 10 | username: string; 11 | roles: string[]; 12 | } 13 | 14 | export interface State extends fromRoot.State { 15 | [securityFeatureKey]: SecurityState; 16 | } 17 | 18 | export const getSecurity = (state: State): any => state[securityFeatureKey]; 19 | 20 | export const getEnabled = (state: State): boolean => state[securityFeatureKey].enabled; 21 | 22 | export const getAuthenticated = (state: State): boolean => state[securityFeatureKey].authenticated; 23 | 24 | export const getUsername = (state: State): string => state[securityFeatureKey].username; 25 | 26 | export const getRoles = (state: State): string[] => state[securityFeatureKey].roles; 27 | 28 | export const getShouldProtect = createSelector(getEnabled, getAuthenticated, (enabled, authenticated) => 29 | !enabled ? false : enabled && !authenticated 30 | ); 31 | 32 | export const initialState: SecurityState = { 33 | enabled: true, 34 | authenticated: false, 35 | username: undefined, 36 | roles: [] 37 | }; 38 | 39 | export const reducer = createReducer( 40 | initialState, 41 | on(SecurityActions.loaded, (state, props) => ({ 42 | enabled: props.enabled, 43 | authenticated: props.authenticated, 44 | username: props.username, 45 | roles: props.roles 46 | })), 47 | on(SecurityActions.logout, state => ({ 48 | enabled: state.enabled, 49 | authenticated: false, 50 | username: undefined, 51 | roles: [] 52 | })) 53 | ); 54 | -------------------------------------------------------------------------------- /ui/src/app/security/support/security.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router'; 3 | import {AboutService} from '../../shared/api/about.service'; 4 | import {SecurityService} from '../service/security.service'; 5 | import {take} from 'rxjs/operators'; 6 | 7 | @Injectable() 8 | export class SecurityGuard implements CanActivate { 9 | constructor( 10 | private router: Router, 11 | private securityService: SecurityService, 12 | private aboutService: AboutService 13 | ) {} 14 | 15 | async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { 16 | const rolesNeeded: string[] = route.data.roles; 17 | const featureNeeded: string = route.data.feature; 18 | if (featureNeeded) { 19 | if ((await this.aboutService.isFeatureEnabled(featureNeeded)) === false) { 20 | this.router.navigate(['feature-disabled']); 21 | } 22 | } 23 | 24 | const canAccess = await this.securityService.canAccess(rolesNeeded); 25 | if (canAccess) { 26 | return true; 27 | } 28 | const securityEnabled = await this.securityService.securityEnabled().pipe(take(1)).toPromise(); 29 | if (securityEnabled) { 30 | const loggedInUser = await this.securityService.loggedinUser().pipe(take(1)).toPromise(); 31 | if (loggedInUser) { 32 | this.router.navigate(['roles-missing']); 33 | } else { 34 | this.router.navigate(['authentication-required']); 35 | } 36 | } else { 37 | return true; 38 | } 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/app/security/support/security.interceptor.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpErrorResponse} from '@angular/common/http'; 3 | import {Observable} from 'rxjs'; 4 | import {tap} from 'rxjs/operators'; 5 | import {SecurityService} from '../service/security.service'; 6 | 7 | @Injectable() 8 | export class SecurityInterceptor implements HttpInterceptor { 9 | constructor(private securityService: SecurityService) {} 10 | 11 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 12 | request = request.clone({ 13 | setHeaders: { 14 | 'X-Requested-With': 'XMLHttpRequest' 15 | } 16 | }); 17 | return next.handle(request).pipe( 18 | tap( 19 | () => {}, 20 | (err: any) => { 21 | if (err instanceof HttpErrorResponse) { 22 | if (err.status !== 401) { 23 | return; 24 | } 25 | this.securityService.unauthorised(); 26 | } 27 | } 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/app/settings/settings-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | import {SecurityGuard} from '../security/support/security.guard'; 4 | import {SettingsComponent} from './settings/settings.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'settings', 9 | component: SettingsComponent, 10 | canActivate: [SecurityGuard] 11 | } 12 | ]; 13 | 14 | @NgModule({ 15 | imports: [RouterModule.forChild(routes)], 16 | exports: [RouterModule] 17 | }) 18 | export class SettingsRoutingModule {} 19 | -------------------------------------------------------------------------------- /ui/src/app/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {StoreModule} from '@ngrx/store'; 4 | import {EffectsModule} from '@ngrx/effects'; 5 | import {ClarityModule} from '@clr/angular'; 6 | import {SharedModule} from '../shared/shared.module'; 7 | import {SettingsRoutingModule} from './settings-routing.module'; 8 | import {SettingsComponent} from './settings/settings.component'; 9 | import * as fromSettings from './store/settings.reducer'; 10 | import {SettingsEffect} from './store/settings.effect'; 11 | import {TranslateModule} from '@ngx-translate/core'; 12 | 13 | @NgModule({ 14 | declarations: [SettingsComponent], 15 | imports: [ 16 | CommonModule, 17 | ClarityModule, 18 | SharedModule, 19 | TranslateModule, 20 | SettingsRoutingModule, 21 | StoreModule.forFeature(fromSettings.settingsFeatureKey, fromSettings.reducer), 22 | EffectsModule.forFeature([SettingsEffect]) 23 | ] 24 | }) 25 | export class SettingsModule {} 26 | -------------------------------------------------------------------------------- /ui/src/app/settings/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; 2 | import {provideMockStore, MockStore} from '@ngrx/store/testing'; 3 | import {HttpClientModule} from '@angular/common/http'; 4 | import {StoreModule} from '@ngrx/store'; 5 | import {ClarityModule} from '@clr/angular'; 6 | import {SettingsComponent} from './settings.component'; 7 | import * as fromSettings from '../store/settings.reducer'; 8 | import {TranslateTestingModule} from 'ngx-translate-testing'; 9 | import TRANSLATIONS from '../../../assets/i18n/en.json'; 10 | import {LocalStorageService} from '../../shared/service/local-storage.service'; 11 | 12 | describe('SettingsComponent', () => { 13 | let component: SettingsComponent; 14 | let fixture: ComponentFixture; 15 | let mockStore: MockStore; 16 | 17 | const initialState = { 18 | [fromSettings.settingsFeatureKey]: { 19 | settings: [ 20 | {name: fromSettings.themeActiveKey, value: 'default'}, 21 | {name: fromSettings.reverseProxyFixKey, value: 'false'} 22 | ] 23 | } 24 | }; 25 | 26 | beforeEach(waitForAsync(() => { 27 | TestBed.configureTestingModule({ 28 | imports: [ 29 | HttpClientModule, 30 | ClarityModule, 31 | StoreModule.forRoot({}), 32 | TranslateTestingModule.withTranslations('en', TRANSLATIONS) 33 | ], 34 | providers: [provideMockStore({initialState}), LocalStorageService], 35 | declarations: [SettingsComponent] 36 | }).compileComponents(); 37 | mockStore = TestBed.inject(MockStore); 38 | })); 39 | 40 | beforeEach(() => { 41 | fixture = TestBed.createComponent(SettingsComponent); 42 | component = fixture.componentInstance; 43 | fixture.detectChanges(); 44 | }); 45 | 46 | it('should create', () => { 47 | expect(component).toBeTruthy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /ui/src/app/settings/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {SettingsService} from '../settings.service'; 3 | import {ModalDialog} from '../../shared/service/modal.service'; 4 | import {SettingModel} from '../../shared/model/setting.model'; 5 | 6 | @Component({ 7 | selector: 'app-settings', 8 | templateUrl: './settings.component.html' 9 | }) 10 | export class SettingsComponent extends ModalDialog implements OnInit { 11 | themeActive: SettingModel; 12 | resultsPerPage: SettingModel; 13 | languageActive: SettingModel; 14 | reverseProxyFix: SettingModel; 15 | languages: Array; 16 | 17 | constructor(private settingsService: SettingsService) { 18 | super(); 19 | } 20 | 21 | ngOnInit(): void { 22 | this.languages = this.settingsService.language; 23 | this.settingsService.getSettings().subscribe(settings => { 24 | this.themeActive = settings.find(st => st.name === 'theme-active'); 25 | this.resultsPerPage = settings.find(st => st.name === 'results-per-page'); 26 | this.languageActive = settings.find(st => st.name === 'language-active'); 27 | this.reverseProxyFix = settings.find(st => st.name === 'reverse-proxy-fix-active'); 28 | }); 29 | } 30 | 31 | themeActiveSettingOnChange(theme: string): void { 32 | this.settingsService.dispatch({name: 'theme-active', value: theme}); 33 | } 34 | 35 | resultPerPageSettingOnChange(count: string): void { 36 | this.settingsService.dispatch({name: 'results-per-page', value: count}); 37 | } 38 | 39 | languageActiveSettingOnChange(language: string): void { 40 | this.settingsService.dispatch({name: 'language-active', value: language}); 41 | } 42 | 43 | reverseProxyFixOnChange(active: string): void { 44 | this.settingsService.dispatch({name: 'reverse-proxy-fix-active', value: active}); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ui/src/app/settings/store/settings.action.ts: -------------------------------------------------------------------------------- 1 | import {createAction, props} from '@ngrx/store'; 2 | import {SettingModel} from '../../shared/model/setting.model'; 3 | 4 | export const loaded = createAction('[Settings] loaded', props<{settings: SettingModel[]}>()); 5 | 6 | export const update = createAction('[Setting] update', props<{setting: SettingModel}>()); 7 | 8 | export const updateError = createAction('[Setting] update error', props<{setting: SettingModel}>()); 9 | 10 | export const updateOk = createAction('[Setting] update ok', props<{setting: SettingModel}>()); 11 | -------------------------------------------------------------------------------- /ui/src/app/settings/store/settings.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import {createAction} from '@ngrx/store'; 2 | import * as fromSettings from './settings.reducer'; 3 | import * as SettingsActions from './settings.action'; 4 | 5 | describe('settings/store/settings.reducer.ts', () => { 6 | it('should return init state', () => { 7 | const newState = fromSettings.reducer(undefined, createAction('noop')); 8 | expect(newState).toEqual(fromSettings.initialState); 9 | }); 10 | 11 | it('should load and update', () => { 12 | let expectedState: fromSettings.SettingsState = { 13 | settings: [ 14 | {name: fromSettings.themeActiveKey, value: 'value1'}, 15 | {name: fromSettings.languageActiveKey, value: 'value1'}, 16 | {name: fromSettings.reverseProxyFixKey, value: 'value1'} 17 | ] 18 | }; 19 | let newState = fromSettings.reducer( 20 | undefined, 21 | SettingsActions.loaded({ 22 | settings: [ 23 | {name: fromSettings.themeActiveKey, value: 'value1'}, 24 | {name: fromSettings.languageActiveKey, value: 'value1'}, 25 | {name: fromSettings.reverseProxyFixKey, value: 'value1'} 26 | ] 27 | }) 28 | ); 29 | expect(newState).toEqual(expectedState); 30 | expectedState = { 31 | settings: [ 32 | {name: fromSettings.themeActiveKey, value: 'value2'}, 33 | {name: fromSettings.languageActiveKey, value: 'value1'}, 34 | {name: fromSettings.reverseProxyFixKey, value: 'value1'} 35 | ] 36 | }; 37 | newState = fromSettings.reducer( 38 | newState, 39 | SettingsActions.update({ 40 | setting: {name: fromSettings.themeActiveKey, value: 'value2'} 41 | }) 42 | ); 43 | expect(newState).toEqual(expectedState); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /ui/src/app/shared/api/about.service.spec.ts: -------------------------------------------------------------------------------- 1 | // import { AboutService } from './about.service'; 2 | // import { of } from 'rxjs'; 3 | // import { LOAD } from '../../tests/data/about'; 4 | // 5 | // describe('shared/api/about.service.ts', () => { 6 | // let mockHttp; 7 | // let aboutService; 8 | // let jsonData = {}; 9 | // 10 | // beforeEach(() => { 11 | // mockHttp = { 12 | // delete: jasmine.createSpy('delete'), 13 | // get: jasmine.createSpy('get'), 14 | // post: jasmine.createSpy('post'), 15 | // put: jasmine.createSpy('put'), 16 | // }; 17 | // jsonData = {}; 18 | // aboutService = new AboutService(mockHttp); 19 | // }); 20 | // 21 | // it('load', async (done) => { 22 | // mockHttp.get.and.returnValue(of(LOAD)); 23 | // await aboutService.load().subscribe(); 24 | // await aboutService.getAbout().subscribe(); 25 | // const httpUri = mockHttp.get.calls.mostRecent().args[0]; 26 | // expect(httpUri).toEqual('/about'); 27 | // done(); 28 | // }); 29 | // 30 | // }); 31 | -------------------------------------------------------------------------------- /ui/src/app/shared/api/about.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | import {catchError, map, take} from 'rxjs/operators'; 4 | import {ErrorUtils} from '../support/error.utils'; 5 | import {Observable, of} from 'rxjs'; 6 | import {select, Store} from '@ngrx/store'; 7 | import {aboutFeatureKey, AboutState, getAbout, getFeatures, getMonitoring, State} from '../store/about.reducer'; 8 | import {loaded} from '../store/about.action'; 9 | import {parse} from '../store/about.support'; 10 | import {LOAD} from '../../tests/data/about'; 11 | import {UrlUtilities} from '../../url-utilities.service'; 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class AboutService { 17 | constructor( 18 | private httpClient: HttpClient, 19 | private store: Store 20 | ) {} 21 | 22 | load(): Observable { 23 | return this.httpClient.get(UrlUtilities.calculateBaseApiUrl() + 'about').pipe( 24 | map(parse), 25 | map((about: AboutState) => { 26 | this.store.dispatch(loaded(about)); 27 | return about; 28 | }), 29 | catchError(ErrorUtils.catchError) 30 | ); 31 | } 32 | 33 | getAbout(): Observable { 34 | return this.store.pipe(select(getAbout)); 35 | } 36 | 37 | async isFeatureEnabled(feature: string): Promise { 38 | const features = await this.store.pipe(select(getFeatures)).pipe(take(1)).toPromise(); 39 | return features[feature] === true; 40 | } 41 | 42 | getMonitoringType(): Observable { 43 | return this.store.pipe(select(state => state[aboutFeatureKey].features.monitoringDashboardType)); 44 | } 45 | 46 | getMonitoring(): Observable { 47 | return this.store.pipe(select(getMonitoring)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ui/src/app/shared/api/runtime.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {RuntimeService} from './runtime.service'; 2 | import {of} from 'rxjs'; 3 | 4 | describe('shared/api/runtime.service.ts', () => { 5 | let mockHttp; 6 | let runtimeService; 7 | let jsonData = {}; 8 | beforeEach(() => { 9 | mockHttp = { 10 | delete: jasmine.createSpy('delete'), 11 | get: jasmine.createSpy('get'), 12 | post: jasmine.createSpy('post'), 13 | put: jasmine.createSpy('put') 14 | }; 15 | jsonData = {}; 16 | runtimeService = new RuntimeService(mockHttp); 17 | }); 18 | 19 | it('getRuntime', () => { 20 | mockHttp.get.and.returnValue(of(jsonData)); 21 | runtimeService.getRuntime(0, 100); 22 | const httpUri = mockHttp.get.calls.mostRecent().args[0]; 23 | const httpParams = mockHttp.get.calls.mostRecent().args[1].params; 24 | expect(httpParams.get('page')).toEqual('0'); 25 | expect(httpParams.get('size')).toEqual('100'); 26 | expect(httpUri).toEqual('/runtime/streams'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /ui/src/app/shared/api/runtime.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpUtils} from '../support/http.utils'; 3 | import {Observable} from 'rxjs'; 4 | import {HttpClient} from '@angular/common/http'; 5 | import {catchError, map} from 'rxjs/operators'; 6 | import {RuntimeStreamPage} from '../model/runtime.model'; 7 | import {ErrorUtils} from '../support/error.utils'; 8 | import {UrlUtilities} from '../../url-utilities.service'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class RuntimeService { 14 | constructor(private httpClient: HttpClient) {} 15 | 16 | getRuntime(page: number, size: number): Observable { 17 | const params = HttpUtils.getPaginationParams(page, size); 18 | const headers = HttpUtils.getDefaultHttpHeaders(); 19 | return this.httpClient 20 | .get(UrlUtilities.calculateBaseApiUrl() + 'runtime/streams', {params, headers}) 21 | .pipe(map(RuntimeStreamPage.parse), catchError(ErrorUtils.catchError)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/app/shared/component/card/card.component.html: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /ui/src/app/shared/component/confirm/confirm.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /ui/src/app/shared/component/confirm/confirm.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; 2 | import {TranslateService} from '@ngx-translate/core'; 3 | 4 | @Component({ 5 | selector: 'app-confirm', 6 | templateUrl: './confirm.component.html', 7 | styles: [] 8 | }) 9 | export class ConfirmComponent { 10 | isOpen = false; 11 | @Output() onConfirmed = new EventEmitter(); 12 | @Input() title; 13 | @Input() yes; 14 | 15 | constructor(private translate: TranslateService) { 16 | this.title = this.translate.instant('commons.confirmAction'); 17 | this.yes = this.translate.instant('commons.yes'); 18 | } 19 | 20 | open(): void { 21 | this.isOpen = true; 22 | } 23 | 24 | confirm(): void { 25 | this.onConfirmed.emit(true); 26 | this.isOpen = false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/app/shared/component/key-value/key-value.interface.ts: -------------------------------------------------------------------------------- 1 | export interface KeyValueValidators { 2 | key: Array<(control) => any>; 3 | 4 | value: Array<(control) => any>; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/shared/component/stream-dsl/stream-dsl.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewEncapsulation, Input} from '@angular/core'; 2 | import {ParserService} from '../../../flo/shared/service/parser.service'; 3 | 4 | @Component({ 5 | selector: 'app-stream-dsl', 6 | template: ` 7 | 8 | {{ shortDslText }} 9 | 10 | 11 | 12 | {{ dslText }} 13 | 14 | 15 | {{ dslText }} 16 | << 17 | 18 | 19 | {{ shortDslText }} 20 | >> 21 | 22 | 23 | `, 24 | encapsulation: ViewEncapsulation.None 25 | }) 26 | export class StreamDslComponent { 27 | opened = false; 28 | state: 'unexpandable' | 'expanded' | 'collapsed'; 29 | dslText: string; 30 | shortDslText: string; 31 | @Input() expandable = true; 32 | 33 | @Input('dsl') 34 | set dsl(dsl: string) { 35 | if (this.dslText !== dsl) { 36 | this.dslText = dsl; 37 | this.shortDslText = this.parserService.simplifyDsl(dsl); 38 | if (this.dsl === this.shortDslText) { 39 | this.state = 'unexpandable'; 40 | } else { 41 | this.state = this.opened ? 'expanded' : 'collapsed'; 42 | } 43 | } 44 | } 45 | 46 | get dsl(): string { 47 | return this.dslText; 48 | } 49 | 50 | constructor(private parserService: ParserService) {} 51 | 52 | getState(): string { 53 | return this.state; 54 | } 55 | 56 | expand(): void { 57 | if (this.state !== 'unexpandable') { 58 | this.state = 'expanded'; 59 | } 60 | } 61 | 62 | collapse(): void { 63 | if (this.state !== 'unexpandable') { 64 | this.state = 'collapsed'; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ui/src/app/shared/component/toast/toast.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 11 | 16 | 21 | 22 |
23 | {{ title }} 24 | [{{ duplicatesCount + 1 }}] 25 |
26 |
33 |
40 | {{ message }} 41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /ui/src/app/shared/component/toast/toast.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Toast, ToastPackage, ToastrService} from 'ngx-toastr'; 3 | import {animate, keyframes, state, style, transition, trigger} from '@angular/animations'; 4 | 5 | @Component({ 6 | selector: 'app-toast', 7 | templateUrl: './toast.component.html', 8 | styleUrls: ['./toast.component.scss'], 9 | animations: [ 10 | trigger('flyInOut', [ 11 | state( 12 | 'inactive', 13 | style({ 14 | opacity: 0 15 | }) 16 | ), 17 | transition( 18 | 'inactive => active', 19 | animate( 20 | '400ms ease-out', 21 | keyframes([ 22 | style({ 23 | transform: 'translate3d(100%, 0, 0)', 24 | opacity: 0 25 | }), 26 | style({ 27 | opacity: 1 28 | }), 29 | style({ 30 | opacity: 1 31 | }), 32 | style({ 33 | transform: 'none', 34 | opacity: 1 35 | }) 36 | ]) 37 | ) 38 | ), 39 | transition( 40 | 'active => removed', 41 | animate( 42 | '400ms ease-out', 43 | keyframes([ 44 | style({ 45 | opacity: 1 46 | }), 47 | style({ 48 | transform: 'translate3d(100%, 0, 0)', 49 | opacity: 0 50 | }) 51 | ]) 52 | ) 53 | ) 54 | ]) 55 | ] 56 | }) 57 | export class ToastComponent extends Toast { 58 | constructor( 59 | protected toastrService: ToastrService, 60 | public toastPackage: ToastPackage 61 | ) { 62 | super(toastrService, toastPackage); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ui/src/app/shared/directive/focus.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, ElementRef, Input, OnChanges, OnDestroy, Optional} from '@angular/core'; 2 | 3 | /** 4 | * This directive is a helper for any situation where you need 5 | * to focus a control. 6 | * 7 | * @author Damien Vitrac 8 | */ 9 | @Directive({ 10 | selector: '[dataflowFocus]' 11 | }) 12 | export class FocusDirective implements OnChanges { 13 | /** 14 | * App Focus 15 | * @type {boolean} 16 | */ 17 | @Input() dataflowFocus = false; 18 | 19 | /** 20 | * Delay Focus 21 | * @type {number} 22 | */ 23 | @Input() focusDelay = 0; 24 | 25 | /** 26 | * Constructor 27 | * @param {ElementRef} elementRef 28 | */ 29 | constructor(private elementRef: ElementRef) {} 30 | 31 | /** 32 | * On Change Event 33 | */ 34 | ngOnChanges(): void { 35 | this.checkFocus(); 36 | } 37 | 38 | /** 39 | * Check Focus 40 | */ 41 | private checkFocus() { 42 | if (this.dataflowFocus && document.activeElement !== this.elementRef.nativeElement) { 43 | const focus = () => { 44 | this.elementRef.nativeElement.focus(); 45 | }; 46 | setTimeout(focus, this.focusDelay); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ui/src/app/shared/directive/tippy.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, Input, OnInit, ElementRef} from '@angular/core'; 2 | import tippy, {Props, Content} from 'tippy.js'; 3 | 4 | @Directive({ 5 | /* eslint-disable-next-line */ 6 | selector: '[tippy]' 7 | }) 8 | export class TippyDirective implements OnInit { 9 | private instance; 10 | 11 | @Input() public tippyOptions: Props; 12 | 13 | @Input('content') 14 | set content(c: Content) { 15 | if (this.instance) { 16 | this.instance.setContent(c); 17 | } 18 | } 19 | 20 | constructor(private el: ElementRef) { 21 | this.el = el; 22 | } 23 | 24 | ngOnInit(): void { 25 | this.instance = tippy(this.el.nativeElement, this.tippyOptions || {}); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/app/shared/model/app.model.ts: -------------------------------------------------------------------------------- 1 | import {Page} from './page.model'; 2 | 3 | export enum ApplicationType { 4 | app, 5 | source, 6 | processor, 7 | sink, 8 | task 9 | } 10 | 11 | /* eslint-disable-next-line */ 12 | export namespace ApplicationType { 13 | export function getKeys(): number[] { 14 | return Object.keys(ApplicationType) 15 | .filter(isNumber) 16 | .map(value => Number(value)); 17 | } 18 | 19 | /* eslint-disable-next-line */ 20 | function isNumber(element, index, array) { 21 | return !isNaN(element); 22 | } 23 | } 24 | 25 | export class App { 26 | name: string; 27 | type: ApplicationType; 28 | uri: string; 29 | metaDataUri: string; 30 | version: string; 31 | defaultVersion: boolean; 32 | versions: Array; 33 | 34 | public static parse(input: any): App { 35 | const app = new App(); 36 | app.name = input.name; 37 | app.type = input.type as ApplicationType; 38 | app.uri = input.uri; 39 | app.version = input.version; 40 | app.defaultVersion = input.defaultVersion; 41 | app.versions = input.versions; 42 | return app; 43 | } 44 | 45 | labelTypeClass(): string { 46 | switch ((this.type || '').toString()) { 47 | case 'source': 48 | return 'label label-app source'; 49 | case 'sink': 50 | return 'label label-app sink'; 51 | case 'processor': 52 | return 'label label-app processor'; 53 | case 'task': 54 | return 'label label-app task'; 55 | default: 56 | case 'app': 57 | return 'label label-app app'; 58 | } 59 | } 60 | } 61 | 62 | export class AppPage extends Page { 63 | public static parse(input: any): Page { 64 | const page = Page.fromJSON(input); 65 | if (input && input._embedded && input._embedded.appRegistrationResourceList) { 66 | page.items = input._embedded.appRegistrationResourceList.map(App.parse); 67 | } 68 | return page; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ui/src/app/shared/model/context.model.ts: -------------------------------------------------------------------------------- 1 | export interface ContextModel { 2 | name: string; 3 | value: any; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/shared/model/error.model.ts: -------------------------------------------------------------------------------- 1 | export class AppError { 2 | message: string; 3 | 4 | constructor(message: string) { 5 | this.message = message; 6 | } 7 | 8 | static is(error: any): error is AppError { 9 | return true; 10 | } 11 | 12 | getMessage(): string { 13 | return this.message; 14 | } 15 | 16 | toString(): string { 17 | return this.message; 18 | } 19 | } 20 | 21 | export class HttpError extends AppError { 22 | httpStatusCode: number; 23 | 24 | constructor(message: string, httpStatusCode: number) { 25 | super(message); 26 | this.httpStatusCode = httpStatusCode; 27 | } 28 | 29 | static is(error: any): error is HttpError { 30 | return true; 31 | } 32 | 33 | static is404(error: any): boolean { 34 | if (HttpError.is(error)) { 35 | return error.httpStatusCode === 404; 36 | } 37 | return false; 38 | } 39 | 40 | static is400(error: any): boolean { 41 | if (HttpError.is(error)) { 42 | return error.httpStatusCode === 400; 43 | } 44 | return false; 45 | } 46 | 47 | getMessage(): string { 48 | return super.getMessage(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ui/src/app/shared/model/instance.model.ts: -------------------------------------------------------------------------------- 1 | import {AppStatus} from './metrics.model'; 2 | 3 | export class Instance { 4 | name: string; 5 | instanceCount: number; 6 | isScaling = false; 7 | 8 | constructor(name: string, instanceCount: number, isScaling: boolean) { 9 | this.name = name; 10 | this.instanceCount = instanceCount; 11 | this.isScaling = isScaling; 12 | } 13 | 14 | static fromAppStatus(appStatus: AppStatus): Instance { 15 | return new Instance(appStatus.name, appStatus.instances.length, false); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/shared/model/page.model.ts: -------------------------------------------------------------------------------- 1 | export class Page { 2 | pages = 0; 3 | total = 0; 4 | page = 1; 5 | size = 20; 6 | items: T[] = []; 7 | 8 | // eslint-disable-next-line 9 | public static fromJSON(input): Page { 10 | const page = new Page(); 11 | if (input && input.page) { 12 | page.page = input.page.number; 13 | page.size = input.page.size; 14 | page.total = input.page.totalElements; 15 | page.pages = input.page.totalPages; 16 | } 17 | return page; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/shared/model/platform.model.ts: -------------------------------------------------------------------------------- 1 | export class Platform { 2 | public name: string; 3 | public type: string; 4 | public description: string; 5 | public options: Array; 6 | 7 | static parse(input: any): Platform { 8 | const platform = new Platform(); 9 | platform.name = input.name; 10 | platform.type = input.type; 11 | platform.description = input.description; 12 | platform.options = input.options; 13 | return platform; 14 | } 15 | } 16 | 17 | export class PlatformList { 18 | static parse(input: any): Array { 19 | if (input && Array.isArray(input)) { 20 | return input.map(Platform.parse); 21 | } 22 | return []; 23 | } 24 | } 25 | 26 | export class PlatformTaskList { 27 | static parse(input: any): Array { 28 | if (input && input._embedded && input._embedded.launcherResourceList) { 29 | return input._embedded.launcherResourceList.map(Platform.parse); 30 | } 31 | return []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/app/shared/model/schedule.model.ts: -------------------------------------------------------------------------------- 1 | import {Page} from './page.model'; 2 | 3 | export class Schedule { 4 | name: string; 5 | taskName: string; 6 | cronExpression: string; 7 | platform: string; 8 | 9 | static parse(input: any): Schedule { 10 | let cron = ''; 11 | if (input.scheduleProperties) { 12 | cron = input.scheduleProperties['spring.cloud.scheduler.cron.expression']; 13 | } 14 | const schedule = new Schedule(); 15 | schedule.name = input.scheduleName; 16 | schedule.taskName = input.taskDefinitionName; 17 | schedule.cronExpression = cron; 18 | schedule.platform = input?.platform; 19 | return schedule; 20 | } 21 | } 22 | 23 | export class SchedulePage extends Page { 24 | static parse(input: any): Page { 25 | const page = Page.fromJSON(input); 26 | if (input && input._embedded && input._embedded.scheduleInfoResourceList) { 27 | page.items = input._embedded.scheduleInfoResourceList.map(Schedule.parse); 28 | } 29 | return page; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/app/shared/model/security.model.ts: -------------------------------------------------------------------------------- 1 | export interface Security { 2 | authenticationEnabled: boolean; 3 | authenticated: boolean; 4 | username: string; 5 | roles: string[]; 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/app/shared/model/setting.model.ts: -------------------------------------------------------------------------------- 1 | export interface SettingModel { 2 | name: string; 3 | value: string; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/shared/pipe/capitalize.pipe.ts: -------------------------------------------------------------------------------- 1 | import {PipeTransform, Pipe} from '@angular/core'; 2 | 3 | /** 4 | * Pipe that transforms an input string to camel-case. 5 | * 6 | * @author Gunnar Hillert 7 | */ 8 | @Pipe({name: 'capitalize'}) 9 | export class CapitalizePipe implements PipeTransform { 10 | transform(input: string): string { 11 | if (input) { 12 | return input.substring(0, 1).toUpperCase() + input.substring(1); 13 | } 14 | return input; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/app/shared/pipe/datagrid-column.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | import {ClrDatagrid} from '@clr/angular'; 3 | import {take} from 'rxjs/operators'; 4 | import {ContextModel} from '../model/context.model'; 5 | import {ContextService} from '../service/context.service'; 6 | 7 | @Pipe({ 8 | name: 'datagridcolumn' 9 | }) 10 | export class DatagridColumnPipe implements PipeTransform { 11 | constructor(private contextService: ContextService) {} 12 | 13 | async transform(value: any, datagrid: ClrDatagrid, contextName: string, gap = 10): Promise { 14 | if (value) { 15 | return +value; 16 | } 17 | const context = await this.contextService.getContext(contextName).pipe(take(1)).toPromise(); 18 | const cols = []; 19 | context.forEach((ct: ContextModel) => { 20 | if (ct.name !== 'size' && ct.name.startsWith('size') && +ct.value > 0) { 21 | cols.push(+ct.value); 22 | } 23 | }); 24 | await setTimeout(() => {}); 25 | const length = datagrid?.columns?.length - cols.length; 26 | const width = 27 | datagrid?.datagridTable?.nativeElement?.clientWidth - cols.reduce((prev, current) => prev + current, 0); 28 | 29 | if (!length || !width) { 30 | return; 31 | } 32 | return Math.max(Math.round(width / length - gap), 50); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/shared/pipe/datetime.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | import {DateTime} from 'luxon'; 3 | 4 | @Pipe({ 5 | name: 'datetime' 6 | }) 7 | export class DatetimePipe implements PipeTransform { 8 | transform(value: string | DateTime, format: string = null): string { 9 | let m: DateTime; 10 | if (value instanceof DateTime) { 11 | m = value; 12 | } else { 13 | m = DateTime.fromISO(value as string); 14 | } 15 | if (m.isValid) { 16 | return m.toFormat(format != null ? format : 'yyyy-MM-dd HH:mm:ss,SSS[Z]'); 17 | } else { 18 | return 'N/A'; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/shared/pipe/duration.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | import {DateTime} from 'luxon'; 3 | 4 | @Pipe({ 5 | name: 'duration' 6 | }) 7 | export class DurationPipe implements PipeTransform { 8 | private DEFAULT = 'hh:mm:ss.SSS'; 9 | transform(start: DateTime, end: DateTime, format: string = null): string { 10 | if (start && end) { 11 | const m = end.diff(start); 12 | if (m.isValid) { 13 | return m.toFormat(format != null ? format : this.DEFAULT); 14 | } 15 | } 16 | return 'N/A'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/shared/pipe/order-by.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'orderby' 5 | }) 6 | export class OrderByPipe implements PipeTransform { 7 | transform(array: Array, sort: string, order?: string): any { 8 | if (!array) { 9 | return; 10 | } 11 | if (!sort) { 12 | return array; 13 | } 14 | if (!order) { 15 | order = 'ASC'; 16 | } 17 | return Object.assign([], array).sort((a: any, b: any) => { 18 | if (order === 'ASC') { 19 | return a[sort] > b[sort] ? 1 : -1; 20 | } else { 21 | return a[sort] <= b[sort] ? 1 : -1; 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/shared/service/clipboard-copy.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable, PLATFORM_ID} from '@angular/core'; 2 | import {isPlatformBrowser} from '@angular/common'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class ClipboardCopyService { 8 | private textareaEl: HTMLTextAreaElement; 9 | 10 | constructor(@Inject(PLATFORM_ID) private platformId: Record) {} 11 | 12 | private createTextareaEl() { 13 | this.textareaEl = document.createElement('textarea'); 14 | 15 | // make it off screen 16 | this.textareaEl.setAttribute('readonly', ''); 17 | this.textareaEl.classList.add('offscreen-clipboard-textarea'); 18 | } 19 | 20 | private setTextareaValue(value: string) { 21 | this.textareaEl.value = value; 22 | } 23 | 24 | executeCopy(copyContent: string, container?: HTMLElement): void { 25 | if (isPlatformBrowser(this.platformId)) { 26 | const _container = container ? container : document.body; 27 | this.createTextareaEl(); 28 | this.setTextareaValue(copyContent); 29 | _container.appendChild(this.textareaEl); 30 | this.textareaEl.select(); 31 | document.execCommand('copy'); 32 | _container.removeChild(this.textareaEl); 33 | delete this.textareaEl; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ui/src/app/shared/service/context.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {select, Store} from '@ngrx/store'; 3 | import {contextFeatureKey, getContext, getContexts, State} from '../store/context.reducer'; 4 | import {updated} from '../store/context.action'; 5 | import {ContextModel} from '../model/context.model'; 6 | import {Observable} from 'rxjs'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ContextService { 12 | constructor(private store: Store) {} 13 | 14 | dispatch(context: ContextModel): void { 15 | this.store.dispatch(updated(context)); 16 | } 17 | 18 | getContext(name: string): Observable { 19 | return this.store.pipe(select(state => (getContext(state[contextFeatureKey], name) as ContextModel[]) || [])); 20 | } 21 | 22 | getContexts(): Observable { 23 | return this.store.pipe(select(getContexts)); 24 | } 25 | 26 | updateContext(name: string, contexts: ContextModel[]): void { 27 | this.store.dispatch(updated({name, value: contexts})); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/app/shared/service/group.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {v4 as uuidv4} from 'uuid'; 3 | import {LocalStorageService} from './local-storage.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class GroupService { 9 | constructor(private localStorageService: LocalStorageService) {} 10 | 11 | create(args: any): string { 12 | const key = 'G-' + uuidv4(); 13 | this.localStorageService.set(key, args); 14 | return key; 15 | } 16 | 17 | isSimilar(str: string): boolean { 18 | str = str || ''; 19 | if (!str.startsWith('G-')) { 20 | return false; 21 | } 22 | const g = 'G-'.length; 23 | if ( 24 | str.length !== 36 + g || 25 | str[8 + g] !== '-' || 26 | str[13 + g] !== '-' || 27 | str[18 + g] !== '-' || 28 | str[23 + g] !== '-' 29 | ) { 30 | return false; 31 | } 32 | return true; 33 | } 34 | 35 | group(name: string): any { 36 | return this.localStorageService.get(name); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/app/shared/service/local-storage.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, inject} from '@angular/core'; 2 | import {DOCUMENT} from '@angular/common'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class LocalStorageService { 8 | private readonly localStorage = inject(DOCUMENT)?.defaultView?.localStorage; 9 | 10 | get(key: string): T | null { 11 | const item = this.localStorage?.getItem(`dataflow-${key}`); 12 | 13 | if (!item) { 14 | return null; 15 | } 16 | 17 | return this.isJSONValid(item) ? (JSON.parse(item) as T) : (item as T); 18 | } 19 | 20 | set(key: string, value: unknown): void { 21 | this.localStorage?.setItem(`dataflow-${key}`, JSON.stringify(value)); 22 | } 23 | 24 | remove(key: string): void { 25 | this.localStorage?.removeItem(key); 26 | } 27 | 28 | removeKeys(keys: string[]): void { 29 | keys.forEach(key => this.localStorage?.removeItem(`dataflow-${key}`)); 30 | } 31 | 32 | clear(): void { 33 | this.localStorage?.clear(); 34 | } 35 | 36 | private isJSONValid(value: string): boolean { 37 | try { 38 | JSON.parse(value); 39 | return true; 40 | } catch (error) { 41 | return false; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/src/app/shared/service/logger.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, isDevMode} from '@angular/core'; 2 | 3 | /** 4 | * A service for global logs. 5 | * 6 | * @author Damien Vitrac 7 | */ 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class LoggerService { 12 | constructor() {} 13 | 14 | static log(value: any, ...rest: any[]): void { 15 | if (isDevMode()) { 16 | console.log(value, ...rest); 17 | } 18 | } 19 | 20 | static error(value: any, ...rest: any[]): void { 21 | console.error(value, ...rest); 22 | } 23 | 24 | static warn(value: any, ...rest: any[]): void { 25 | console.warn(value, ...rest); 26 | } 27 | 28 | log(value: any, ...rest: any[]): void { 29 | LoggerService.log(value, ...rest); 30 | } 31 | 32 | error(value: any, ...rest: any[]): void { 33 | LoggerService.error(value, ...rest); 34 | } 35 | 36 | warn(value: any, ...rest: any[]): void { 37 | LoggerService.warn(value, ...rest); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/app/shared/service/modal.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationRef, 3 | ComponentFactoryResolver, 4 | ComponentRef, 5 | Inject, 6 | EventEmitter, 7 | Injectable, 8 | Injector, 9 | Type 10 | } from '@angular/core'; 11 | import {DOCUMENT} from '@angular/common'; 12 | import {Subject} from 'rxjs'; 13 | 14 | export class ModalDialog { 15 | private _open = false; 16 | private _opened = new EventEmitter(); 17 | 18 | set isOpen(open: boolean) { 19 | if (this._open !== open) { 20 | this._open = open; 21 | this._opened.emit(open); 22 | } 23 | } 24 | 25 | get isOpen(): boolean { 26 | return this._open; 27 | } 28 | 29 | get opened(): Subject { 30 | return this._opened; 31 | } 32 | } 33 | 34 | @Injectable({ 35 | providedIn: 'root' 36 | }) 37 | export class ModalService { 38 | constructor( 39 | @Inject(DOCUMENT) private document: Document, 40 | private componentFactoryResolver: ComponentFactoryResolver, 41 | private injector: Injector, 42 | private applicationRef: ApplicationRef 43 | ) {} 44 | 45 | public show(componentType: Type): T { 46 | const nodeComponentFactory = this.componentFactoryResolver.resolveComponentFactory(componentType); 47 | const componentRef: ComponentRef = nodeComponentFactory.create(this.injector); 48 | componentRef.changeDetectorRef.markForCheck(); 49 | componentRef.changeDetectorRef.detectChanges(); 50 | this.applicationRef.attachView(componentRef.hostView); 51 | this.document.body.appendChild(componentRef.location.nativeElement); 52 | const subscription = componentRef.instance.opened.subscribe(open => { 53 | if (!open) { 54 | this.document.body.removeChild(componentRef.location.nativeElement); 55 | this.applicationRef.detachView(componentRef.hostView); 56 | componentRef.destroy(); 57 | subscription.unsubscribe(); 58 | } 59 | }); 60 | componentRef.instance.isOpen = true; 61 | return componentRef.instance; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ui/src/app/shared/service/notification.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ToastrService} from 'ngx-toastr'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class NotificationService { 8 | constructor(private toastrService: ToastrService) {} 9 | 10 | success(title: string, message: string): void { 11 | this.toastrService.success(message, title); 12 | } 13 | 14 | info(title: string, message: string): void { 15 | this.toastrService.info(message, title); 16 | } 17 | 18 | error(title: string, message: string): void { 19 | this.toastrService.error(message, title); 20 | } 21 | 22 | warning(title: string, message: string): void { 23 | this.toastrService.warning(message, title); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/shared/store/about.action.ts: -------------------------------------------------------------------------------- 1 | import {createAction, props} from '@ngrx/store'; 2 | import {AboutState} from './about.reducer'; 3 | 4 | export const loaded = createAction('[About] Loaded', props()); 5 | -------------------------------------------------------------------------------- /ui/src/app/shared/store/context.action.ts: -------------------------------------------------------------------------------- 1 | import {createAction, props} from '@ngrx/store'; 2 | import {ContextModel} from '../model/context.model'; 3 | 4 | export const updated = createAction('[Context] update', props()); 5 | -------------------------------------------------------------------------------- /ui/src/app/shared/support/encoder.utils.ts: -------------------------------------------------------------------------------- 1 | import {HttpParameterCodec} from '@angular/common/http'; 2 | 3 | /** 4 | * Custom HTTP Parameter Encoder 5 | */ 6 | export class DataflowEncoder implements HttpParameterCodec { 7 | encodeKey(key: string): string { 8 | return encodeURIComponent(key); 9 | } 10 | 11 | encodeValue(value: string): string { 12 | return encodeURIComponent(value); 13 | } 14 | 15 | decodeKey(key: string): string { 16 | return decodeURIComponent(key); 17 | } 18 | 19 | decodeValue(value: string): string { 20 | return decodeURIComponent(value); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/app/shared/support/error.utils.ts: -------------------------------------------------------------------------------- 1 | import {HttpErrorResponse} from '@angular/common/http'; 2 | import {throwError} from 'rxjs'; 3 | import {AppError, HttpError} from '../model/error.model'; 4 | import get from 'lodash.get'; 5 | 6 | export class ErrorUtils { 7 | public static catchError(error: Response | any): any { 8 | const errorObject = { 9 | status: 0, 10 | message: '' 11 | }; 12 | if (error instanceof HttpErrorResponse) { 13 | let body; 14 | errorObject.status = error.status; 15 | try { 16 | body = get(error, 'error', ''); 17 | if (get(body, '_embedded.errors')) { 18 | body = get(body, '_embedded.errors'); 19 | } 20 | } catch (e) { 21 | errorObject.message = `${error} (Status code: ${error.status})`; 22 | } 23 | if (body) { 24 | let isFirst = true; 25 | for (const bodyElement of body) { 26 | if (!isFirst) { 27 | errorObject.message += '\n'; 28 | } 29 | errorObject.message += bodyElement.message; 30 | isFirst = false; 31 | } 32 | } 33 | return throwError(new HttpError(errorObject.message, errorObject.status)); 34 | } else { 35 | errorObject.message = error.message ? error.message : error.toString(); 36 | return throwError(new AppError(errorObject.message)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/app/shared/support/http.utils.ts: -------------------------------------------------------------------------------- 1 | import {HttpHeaders, HttpParams} from '@angular/common/http'; 2 | 3 | /** 4 | * Contains common HTTP-related helper methods. 5 | * 6 | * @author Gunnar Hillert 7 | */ 8 | export class HttpUtils { 9 | public static getDefaultHttpHeaders(): HttpHeaders { 10 | const headers = new HttpHeaders({ 11 | 'Content-Type': 'application/json', 12 | Accept: 'application/json' 13 | }); 14 | return headers; 15 | } 16 | 17 | public static getPaginationParams(page: number, size: number): HttpParams { 18 | const params = new HttpParams().append('page', page.toString()).append('size', size.toString()); 19 | 20 | return params; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/app/streams/runtime/details/details.component.scss: -------------------------------------------------------------------------------- 1 | h4 { 2 | margin-top: 0; 3 | margin-bottom: 1rem; 4 | } 5 | 6 | input { 7 | background: var(--clr-forms-textarea-background-color, #FFF); 8 | display: inline-block; 9 | width: 100%; 10 | border: 1px solid var(--clr-forms-border-color, #b3b3b3); 11 | padding: 1px 8px; 12 | color: var(--clr-forms-text-color,#000); 13 | 14 | &:focus { 15 | box-shadow: none; 16 | outline: none; 17 | border: 1px solid var(--clr-forms-focused-color, #0072a3); 18 | } 19 | } 20 | 21 | .clr-row { 22 | padding: .3rem 0; 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/app/streams/runtime/details/details.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {RuntimeApp} from '../../../shared/model/runtime.model'; 3 | 4 | @Component({ 5 | selector: 'app-runtime-details', 6 | templateUrl: './details.component.html', 7 | styleUrls: ['./details.component.scss'] 8 | }) 9 | export class DetailsComponent { 10 | runtimeApp: RuntimeApp; 11 | isOpen = false; 12 | 13 | constructor() {} 14 | 15 | open(runtimeApp: RuntimeApp): void { 16 | this.runtimeApp = runtimeApp; 17 | this.isOpen = true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/streams/runtime/runtime.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ViewChild} from '@angular/core'; 2 | import {TranslateService} from '@ngx-translate/core'; 3 | import {NotificationService} from '../../shared/service/notification.service'; 4 | import {RuntimeService} from '../../shared/api/runtime.service'; 5 | import {RuntimeApp, RuntimeStreamPage} from '../../shared/model/runtime.model'; 6 | import {DetailsComponent} from './details/details.component'; 7 | 8 | @Component({ 9 | selector: 'app-runtime', 10 | templateUrl: './runtime.component.html' 11 | }) 12 | export class RuntimeComponent implements OnInit { 13 | loading = true; 14 | page: RuntimeStreamPage; 15 | @ViewChild('detailsModal', {static: true}) detailsModal: DetailsComponent; 16 | 17 | constructor( 18 | private runtimeService: RuntimeService, 19 | private notificationService: NotificationService, 20 | private translate: TranslateService 21 | ) {} 22 | 23 | ngOnInit(): void { 24 | this.refresh(); 25 | } 26 | 27 | refresh(): void { 28 | this.loading = true; 29 | this.runtimeService.getRuntime(0, 100000).subscribe( 30 | (page: RuntimeStreamPage) => { 31 | this.page = page; 32 | this.loading = false; 33 | }, 34 | error => { 35 | this.notificationService.error(this.translate.instant('commons.message.error'), error); 36 | } 37 | ); 38 | } 39 | 40 | details(runtimeApp: RuntimeApp): any { 41 | this.detailsModal.open(runtimeApp); 42 | return false; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/src/app/streams/streams-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | import {RuntimeComponent} from './runtime/runtime.component'; 4 | import {StreamsComponent} from './streams/streams.component'; 5 | import {StreamComponent} from './streams/stream/stream.component'; 6 | import {CreateComponent} from './streams/create/create.component'; 7 | import {DeployComponent} from './streams/deploy/deploy.component'; 8 | import {SecurityGuard} from '../security/support/security.guard'; 9 | import {MultiDeployComponent} from './streams/multi-deploy/multi-deploy.component'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: 'streams', 14 | canActivate: [SecurityGuard], 15 | data: { 16 | authenticate: true, 17 | roles: ['ROLE_VIEW'], 18 | feature: 'streams' 19 | }, 20 | children: [ 21 | { 22 | path: 'list', 23 | component: StreamsComponent 24 | }, 25 | { 26 | path: 'list/create', 27 | component: CreateComponent 28 | }, 29 | { 30 | path: 'list/:name', 31 | component: StreamComponent 32 | }, 33 | { 34 | path: 'list/:name/deploy', 35 | component: DeployComponent 36 | }, 37 | { 38 | path: 'list/:group/multi-deploy', 39 | component: MultiDeployComponent 40 | }, 41 | { 42 | path: 'runtime', 43 | component: RuntimeComponent 44 | } 45 | ] 46 | } 47 | ]; 48 | 49 | @NgModule({ 50 | imports: [RouterModule.forChild(routes)], 51 | exports: [RouterModule] 52 | }) 53 | export class StreamsRoutingModule {} 54 | -------------------------------------------------------------------------------- /ui/src/app/streams/streams/deploy/builder/errors/errors.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ line.property }} 4 | 5 | 6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /ui/src/app/streams/streams/deploy/builder/errors/errors.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .alert-danger { 3 | border: 1px solid #ddacac; 4 | padding: 2px 10px; 5 | flex: none; 6 | display: block; 7 | div.alert-line { 8 | vertical-align: middle; 9 | border-bottom: 1px solid #ddacac; 10 | padding: 5px 0; 11 | display: block; 12 | a.trash { 13 | color: #a34a49; 14 | display: inline-block; 15 | float: right; 16 | padding: 2px 10px; 17 | cursor: pointer; 18 | &:hover { 19 | color: #8d2624; 20 | } 21 | } 22 | &:last-child { 23 | border-bottom: 0 none; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/streams/streams/deploy/builder/errors/errors.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; 2 | 3 | /** 4 | * ErrorsComponent 5 | * 6 | * @author Damien Vitrac 7 | */ 8 | @Component({ 9 | selector: 'app-stream-deploy-builder-errors', 10 | templateUrl: 'errors.component.html', 11 | styleUrls: ['errors.component.scss'], 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | export class ErrorsComponent { 15 | /** 16 | * Error Object 17 | */ 18 | @Input() errors: any; 19 | 20 | /** 21 | * Event triggered to remove a property 22 | */ 23 | @Output() removeError = new EventEmitter<{type: string; index: number}>(); 24 | 25 | removeProperty(type: string, index: number): void { 26 | this.removeError.emit({type, index}); 27 | } 28 | 29 | /** 30 | * List all errors 31 | */ 32 | getErrors(): Array<{type: string; index: number; property: string}> { 33 | const result = []; 34 | if (this.errors) { 35 | this.errors.global.forEach((error, index) => { 36 | result.push({type: 'global', index, property: error}); 37 | }); 38 | this.errors.app.forEach((error, index) => { 39 | result.push({type: 'app', index, property: error}); 40 | }); 41 | } 42 | return result.sort((a, b) => (a.property > b.property ? 1 : -1)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/src/app/streams/streams/deploy/free-text/free-text.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .form-textarea { 3 | $h: 400px; 4 | border: 1px solid #ccc; 5 | display: flex; 6 | min-height: $h; 7 | border-radius: 3px; 8 | margin-top: .6rem; 9 | overflow: auto; 10 | 11 | div.numbers { 12 | width: 50px; 13 | min-height: $h; 14 | background: var(--clr-global-app-background, #fafafa); 15 | flex: 0 0 40px; 16 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 17 | font-size: 14px; 18 | padding: 10px 0 10px; 19 | border-top-left-radius: 3px; 20 | border-bottom-left-radius: 3px; 21 | 22 | .number { 23 | display: block; 24 | text-align: right; 25 | 26 | span { 27 | display: inline-block; 28 | line-height: 18px; 29 | padding: 5px 10px 0; 30 | 31 | &.invalid { 32 | background: var(--clr-forms-invalid-color, #c21d00); 33 | color: var(--clr-forms-textarea-background-color, #fff); 34 | } 35 | } 36 | } 37 | } 38 | 39 | textarea { 40 | margin: 0; 41 | overflow-y: hidden; 42 | overflow-x: auto; 43 | white-space: pre; 44 | border: 0 none; 45 | border-radius: 0; 46 | height: 20px; 47 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 48 | font-size: 14px; 49 | padding: 10px 10px 0; 50 | width: 100%; 51 | outline: none; 52 | line-height: 24px; 53 | 54 | color: var(--clr-forms-text-color, #000); 55 | background: var(--clr-forms-textarea-background-color, #fff); 56 | } 57 | } 58 | 59 | .bar { 60 | padding: .4rem 0; 61 | .btn { 62 | display: inline-block; 63 | width: auto; 64 | flex: none; 65 | } 66 | } 67 | 68 | .btn-file { 69 | width: auto; 70 | flex: none; 71 | 72 | input { 73 | display: none; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ui/src/app/streams/streams/multi-deploy/multi-deploy.component.scss: -------------------------------------------------------------------------------- 1 | .multi-deploy { 2 | .card-block { 3 | padding-top: 0; 4 | padding-bottom: 1.2rem; 5 | } 6 | .actions { 7 | padding: 1.2rem 0 .5rem 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/streams/streams/rollback/rollback.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /ui/src/app/streams/streams/rollback/rollback.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Output} from '@angular/core'; 2 | import {StreamHistory} from '../../../shared/model/stream.model'; 3 | import {StreamService} from '../../../shared/api/stream.service'; 4 | import {NotificationService} from '../../../shared/service/notification.service'; 5 | import {TranslateService} from '@ngx-translate/core'; 6 | 7 | @Component({ 8 | selector: 'app-stream-rollback', 9 | templateUrl: './rollback.component.html', 10 | styles: [] 11 | }) 12 | export class RollbackComponent { 13 | history: StreamHistory; 14 | isOpen = false; 15 | isRunning = false; 16 | @Output() onRollback = new EventEmitter(); 17 | 18 | constructor( 19 | private streamService: StreamService, 20 | private notificationService: NotificationService, 21 | private translation: TranslateService 22 | ) {} 23 | 24 | open(history: StreamHistory): void { 25 | this.history = history; 26 | this.isOpen = true; 27 | } 28 | 29 | close(): void { 30 | this.isOpen = false; 31 | } 32 | 33 | rollback(): void { 34 | this.isRunning = true; 35 | this.streamService.rollbackStream(this.history).subscribe( 36 | data => { 37 | this.notificationService.success( 38 | this.translation.instant('streams.rollback.message.successTitle'), 39 | this.translation.instant('streams.rollback.message.successContent', {version: this.history.version}) 40 | ); 41 | this.onRollback.emit(true); 42 | this.close(); 43 | }, 44 | error => { 45 | this.notificationService.error(this.translation.instant('commons.message.error'), error); 46 | this.isRunning = false; 47 | } 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ui/src/app/streams/streams/scale/scale.component.scss: -------------------------------------------------------------------------------- 1 | .ng-invalid { 2 | border-bottom-color: var(--clr-forms-invalid-color, #c21d00); 3 | } 4 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/executions/cleanup/cleanup.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Output} from '@angular/core'; 2 | import {TaskExecution} from '../../../shared/model/task-execution.model'; 3 | import {TaskService} from '../../../shared/api/task.service'; 4 | import {NotificationService} from '../../../shared/service/notification.service'; 5 | import {TranslateService} from '@ngx-translate/core'; 6 | 7 | @Component({ 8 | selector: 'app-execution-cleanup', 9 | templateUrl: './cleanup.component.html' 10 | }) 11 | export class CleanupComponent { 12 | isOpen = false; 13 | isRunning = false; 14 | executions: TaskExecution[]; 15 | @Output() onCleaned = new EventEmitter(); 16 | 17 | constructor( 18 | private taskService: TaskService, 19 | private notificationService: NotificationService, 20 | private translate: TranslateService 21 | ) {} 22 | 23 | open(executions: TaskExecution[]): void { 24 | this.executions = executions; 25 | this.isRunning = false; 26 | this.isOpen = true; 27 | } 28 | 29 | clean(): void { 30 | this.isRunning = true; 31 | this.taskService.executionsClean(this.executions).subscribe( 32 | data => { 33 | this.notificationService.success( 34 | this.translate.instant('executions.cleanup.message.successTitle'), 35 | this.translate.instant('executions.cleanup.message.successContent', { 36 | count: typeof data === 'object' ? data.length : data 37 | }) 38 | ); 39 | this.onCleaned.emit(data); 40 | this.isOpen = false; 41 | this.executions = null; 42 | }, 43 | error => { 44 | this.notificationService.error(this.translate.instant('commons.message.error'), error); 45 | this.isOpen = false; 46 | this.executions = null; 47 | } 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/executions/execution/log/log.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {ActivatedRoute, Router} from '@angular/router'; 3 | import {TaskService} from '../../../../shared/api/task.service'; 4 | import {TaskExecution} from '../../../../shared/model/task-execution.model'; 5 | 6 | @Component({ 7 | selector: 'app-task-execution-log', 8 | template: ` 9 | 10 | 19 | 24 | ` 25 | }) 26 | export class LogComponent { 27 | loading = true; 28 | isOpen = false; 29 | title = ''; 30 | logs; 31 | 32 | constructor( 33 | private route: ActivatedRoute, 34 | private router: Router, 35 | private taskService: TaskService 36 | ) {} 37 | 38 | open(title: string, execution: TaskExecution): void { 39 | this.loading = true; 40 | this.isOpen = true; 41 | this.title = title; 42 | this.logs = null; 43 | this.taskService.getExecutionLogs(execution).subscribe(logs => { 44 | this.logs = logs; 45 | this.loading = false; 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/executions/stop/stop.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 12 | 20 | 21 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/executions/stop/stop.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, OnInit, Output} from '@angular/core'; 2 | import {TaskExecution} from '../../../shared/model/task-execution.model'; 3 | import {TaskService} from '../../../shared/api/task.service'; 4 | import {NotificationService} from '../../../shared/service/notification.service'; 5 | import {TranslateService} from '@ngx-translate/core'; 6 | 7 | @Component({ 8 | selector: 'app-execution-stop', 9 | templateUrl: './stop.component.html' 10 | }) 11 | export class StopComponent { 12 | isRunning = false; 13 | isOpen = false; 14 | execution: TaskExecution; 15 | @Output() onStopped = new EventEmitter(); 16 | 17 | constructor( 18 | private taskService: TaskService, 19 | private notificationService: NotificationService, 20 | private translate: TranslateService 21 | ) {} 22 | 23 | open(execution: TaskExecution): void { 24 | this.execution = execution; 25 | this.isRunning = false; 26 | this.isOpen = true; 27 | } 28 | 29 | stop(): void { 30 | this.isRunning = true; 31 | this.taskService.executionStop(this.execution).subscribe( 32 | () => { 33 | this.notificationService.success( 34 | this.translate.instant('executions.stop.message.successTitle'), 35 | this.translate.instant('executions.stop.message.successContent', {id: this.execution.executionId}) 36 | ); 37 | this.onStopped.emit(true); 38 | this.isOpen = false; 39 | this.execution = null; 40 | this.isRunning = false; 41 | }, 42 | error => { 43 | this.notificationService.error( 44 | this.translate.instant('commons.message.error'), 45 | this.translate.instant('executions.stop.message.errorContent') 46 | ); 47 | this.isOpen = false; 48 | this.execution = null; 49 | this.isRunning = false; 50 | } 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/schedules/create/create.validator.ts: -------------------------------------------------------------------------------- 1 | import {UntypedFormArray, UntypedFormControl} from '@angular/forms'; 2 | 3 | export class SchedulesCreateValidator { 4 | static cron(formControl: UntypedFormControl): any { 5 | if (!formControl.value) { 6 | return null; 7 | } 8 | return null; 9 | } 10 | 11 | static uniqueName(formArray: UntypedFormArray): any { 12 | const arr = formArray.value as Array; 13 | return arr 14 | .map(item => arr.filter(a => !!item && a.toLowerCase() === item.toLowerCase()).length) 15 | .filter(item => item > 1).length > 0 16 | ? {notUnique: true} 17 | : null; 18 | } 19 | 20 | static existName(control: UntypedFormControl, names: Array): any { 21 | return !!control.value && names.indexOf(control.value.toLowerCase()) > -1 ? {exist: true} : null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/schedules/platform.filter.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | import {Observable, Subject} from 'rxjs'; 3 | import {TaskService} from '../../shared/api/task.service'; 4 | import {Platform} from '../../shared/model/platform.model'; 5 | 6 | @Component({ 7 | selector: 'app-clr-datagrid-platform-filter', 8 | template: `
9 | 10 | 11 | 12 | 13 |
` 14 | }) 15 | export class PlatformFilterComponent implements OnInit { 16 | private pchanges = new Subject(); 17 | property = 'platform'; 18 | @Input() value = null; 19 | val = ''; 20 | platforms: Platform[]; 21 | 22 | constructor(private taskService: TaskService) {} 23 | 24 | ngOnInit(): void { 25 | this.taskService.getPlatforms().subscribe((platforms: Platform[]) => { 26 | this.platforms = platforms; 27 | 28 | if (!this.value) { 29 | this.value = this.platforms[0].name; 30 | } 31 | this.val = this.value; 32 | this.pchanges.next(true); 33 | }); 34 | } 35 | 36 | public get changes(): Observable { 37 | return this.pchanges.asObservable(); 38 | } 39 | 40 | change(): void { 41 | if (this.val === 'all') { 42 | this.value = null; 43 | } else { 44 | this.value = this.val as any; 45 | } 46 | this.pchanges.next(true); 47 | } 48 | 49 | accepts(): boolean { 50 | return true; 51 | } 52 | 53 | isActive(): boolean { 54 | return this.value !== null; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/tasks/cleanup/cleanup.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 17 | 21 | 25 | 40 | 41 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/tasks/launch/builder/errors/errors.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ line.property }} 4 | 5 | 6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/tasks/launch/builder/errors/errors.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .alert-danger { 3 | border: 1px solid #ddacac; 4 | padding: 2px 10px; 5 | flex: none; 6 | display: block; 7 | div.alert-line { 8 | vertical-align: middle; 9 | border-bottom: 1px solid #ddacac; 10 | padding: 5px 0; 11 | display: block; 12 | a.trash { 13 | color: #a34a49; 14 | display: inline-block; 15 | float: right; 16 | padding: 2px 10px; 17 | cursor: pointer; 18 | &:hover { 19 | color: #8d2624; 20 | } 21 | } 22 | &:last-child { 23 | border-bottom: 0 none; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/tasks/launch/builder/errors/errors.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; 2 | 3 | /** 4 | * ErrorsComponent 5 | * 6 | * @author Damien Vitrac 7 | */ 8 | @Component({ 9 | selector: 'app-task-deploy-builder-errors', 10 | templateUrl: 'errors.component.html', 11 | styleUrls: ['errors.component.scss'], 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | export class ErrorsComponent { 15 | /** 16 | * Error Object 17 | */ 18 | @Input() errors: any; 19 | 20 | /** 21 | * Event triggered to remove a property 22 | */ 23 | @Output() removeError = new EventEmitter<{type: string; index: number}>(); 24 | 25 | removeProperty(type: string, index: number): void { 26 | this.removeError.emit({type, index}); 27 | } 28 | 29 | /** 30 | * List all errors 31 | */ 32 | getErrors(): Array { 33 | const result = []; 34 | if (this.errors) { 35 | this.errors.global.forEach((error, index) => { 36 | result.push({type: 'global', index, property: error}); 37 | }); 38 | this.errors.app.forEach((error, index) => { 39 | result.push({type: 'app', index, property: error}); 40 | }); 41 | } 42 | return result.sort((a, b) => (a.property > b.property ? 1 : -1)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/tasks/launch/free-text/free-text.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .form-textarea { 3 | $h: 200px; 4 | border: 1px solid #ccc; 5 | display: flex; 6 | min-height: $h; 7 | border-radius: 3px; 8 | margin-top: .6rem; 9 | overflow: auto; 10 | 11 | div.numbers { 12 | width: 50px; 13 | min-height: $h; 14 | background: var(--clr-global-app-background, #fafafa); 15 | flex: 0 0 40px; 16 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 17 | font-size: 14px; 18 | padding: 10px 0 10px; 19 | border-top-left-radius: 3px; 20 | border-bottom-left-radius: 3px; 21 | 22 | .number { 23 | display: block; 24 | text-align: right; 25 | 26 | span { 27 | display: inline-block; 28 | line-height: 18px; 29 | padding: 5px 10px 0; 30 | 31 | &.invalid { 32 | background: var(--clr-forms-invalid-color, #c21d00); 33 | color: var(--clr-forms-textarea-background-color, #fff); 34 | } 35 | } 36 | } 37 | } 38 | 39 | textarea { 40 | margin: 0; 41 | overflow-y: hidden; 42 | overflow-x: auto; 43 | white-space: pre; 44 | border: 0 none; 45 | border-radius: 0; 46 | height: 20px; 47 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 48 | font-size: 14px; 49 | padding: 10px 10px 0; 50 | width: 100%; 51 | outline: none; 52 | line-height: 24px; 53 | 54 | color: var(--clr-forms-text-color, #000); 55 | background: var(--clr-forms-textarea-background-color, #fff); 56 | } 57 | } 58 | 59 | .bar { 60 | padding: .4rem 0; 61 | .btn { 62 | display: inline-block; 63 | width: auto; 64 | flex: none; 65 | } 66 | } 67 | 68 | .btn-file { 69 | width: auto; 70 | flex: none; 71 | 72 | input { 73 | display: none; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/tasks/task-prop.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import {UntypedFormControl} from '@angular/forms'; 2 | import {TaskPropValidator} from './task-prop.validator'; 3 | 4 | describe('tasks-jobs/tasks/task-prop.validator.ts', () => { 5 | describe('key', () => { 6 | it('invalid', () => { 7 | ['foo', 'foo.bar', 'apps.aaa'].forEach(mock => { 8 | const control: UntypedFormControl = new UntypedFormControl(mock); 9 | expect(TaskPropValidator.key(control).invalid).toBeTruthy(); 10 | }); 11 | }); 12 | it('valid', () => { 13 | ['app.foo', 'deployer.foo', 'scheduler.foo', 'version..foo'].forEach(mock => { 14 | const control: UntypedFormControl = new UntypedFormControl(mock); 15 | expect(TaskPropValidator.key(control)).toBeNull(); 16 | }); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /ui/src/app/tasks-jobs/tasks/task-prop.validator.ts: -------------------------------------------------------------------------------- 1 | import {UntypedFormControl} from '@angular/forms'; 2 | 3 | export class TaskPropValidator { 4 | static key(control: UntypedFormControl): any { 5 | const value = control.value; 6 | if ( 7 | value && 8 | !value.startsWith('app.') && 9 | !value.startsWith('deployer.') && 10 | !value.startsWith('scheduler.') && 11 | !value.startsWith('version.') 12 | ) { 13 | return {invalid: true}; 14 | } 15 | return null; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/tests/api/about.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Observable, of} from 'rxjs'; 3 | 4 | import {AboutService} from '../../shared/api/about.service'; 5 | import {LOAD} from '../data/about'; 6 | import {AboutState} from '../../shared/store/about.reducer'; 7 | import {parse} from '../../shared/store/about.support'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class AboutServiceMock { 13 | static mock: AboutServiceMock = null; 14 | 15 | getAbout(): Observable { 16 | return of(parse(LOAD)); 17 | } 18 | 19 | load(): Observable { 20 | return of(parse(LOAD)); 21 | } 22 | 23 | async isFeatureEnabled(feature: string): Promise { 24 | return true; 25 | } 26 | 27 | getMonitoringType(): Observable { 28 | return of('none'); 29 | } 30 | 31 | getMonitoring(): Observable { 32 | return of({}); 33 | } 34 | 35 | static get provider(): any { 36 | if (!AboutServiceMock.mock) { 37 | AboutServiceMock.mock = new AboutServiceMock(); 38 | } 39 | return {provide: AboutService, useValue: AboutServiceMock.mock}; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/app/tests/api/content-assist.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {ContentAssistService} from '../../flo/stream/content-assist.service'; 2 | import {Observable, of} from 'rxjs'; 3 | 4 | export class ContentAssistServiceMock { 5 | static mock: any = null; 6 | 7 | constructor() {} 8 | 9 | getProposals(prefix: string): Observable> { 10 | return of(['foo', 'bar']); 11 | } 12 | 13 | static get provider(): any { 14 | if (!ContentAssistServiceMock.mock) { 15 | ContentAssistServiceMock.mock = new ContentAssistServiceMock(); 16 | } 17 | return {provide: ContentAssistService, useValue: ContentAssistServiceMock.mock}; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/tests/api/job.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {JobService} from '../../shared/api/job.service'; 2 | import {Observable, of} from 'rxjs'; 3 | import { 4 | ExecutionStepProgress, 5 | ExecutionStepResource, 6 | JobExecution, 7 | JobExecutionPage 8 | } from '../../shared/model/job.model'; 9 | import {GET_EXECUTION, GET_JOBS_EXECUTIONS, GET_PROGRESS, GET_STEP} from '../data/job'; 10 | import {catchError, delay, map} from 'rxjs/operators'; 11 | import {ErrorUtils} from '../../shared/support/error.utils'; 12 | import {TaskExecution} from 'src/app/shared/model/task-execution.model'; 13 | 14 | export class JobServiceMock { 15 | static mock: JobServiceMock = null; 16 | 17 | getExecutions(page: number, size: number): Observable { 18 | return of(GET_JOBS_EXECUTIONS).pipe(delay(1), map(JobExecutionPage.parse), catchError(ErrorUtils.catchError)); 19 | } 20 | 21 | getExecution(executionId: TaskExecution): Observable { 22 | return of(GET_EXECUTION).pipe(delay(1), map(JobExecution.parse), catchError(ErrorUtils.catchError)); 23 | } 24 | 25 | restart(execution: JobExecution): Observable { 26 | return of(null); 27 | } 28 | 29 | stop(item: JobExecution): Observable { 30 | return of(null); 31 | } 32 | 33 | getExecutionStep(jobExecutionId: string, stepId: string): Observable { 34 | return of(GET_STEP).pipe(map(ExecutionStepResource.parse), catchError(ErrorUtils.catchError)); 35 | } 36 | 37 | getExecutionStepProgress(jobExecutionId: string, stepId: string): Observable { 38 | return of(GET_PROGRESS).pipe(map(ExecutionStepProgress.parse), catchError(ErrorUtils.catchError)); 39 | } 40 | 41 | static get provider(): any { 42 | if (!JobServiceMock.mock) { 43 | JobServiceMock.mock = new JobServiceMock(); 44 | } 45 | return {provide: JobService, useValue: JobServiceMock.mock}; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/app/tests/api/record.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {RecordService} from '../../shared/api/record.service'; 2 | import {DateTime} from 'luxon'; 3 | import {Observable, of} from 'rxjs'; 4 | import {RecordActionType, RecordOperationType, RecordPage} from '../../shared/model/record.model'; 5 | import {catchError, delay, map} from 'rxjs/operators'; 6 | import {ErrorUtils} from '../../shared/support/error.utils'; 7 | import {GET_ACTION_TYPES, GET_OPERATION_TYPES, GET_RECORDS} from '../data/record'; 8 | 9 | export class RecordServiceMock { 10 | static mock: RecordServiceMock = null; 11 | 12 | getRecords( 13 | page: number, 14 | size: number, 15 | search?: string, 16 | action?: string, 17 | operation?: string, 18 | fromDate?: DateTime, 19 | toDate?: DateTime, 20 | sort?: string, 21 | order?: string 22 | ): Observable { 23 | return of(GET_RECORDS).pipe(delay(1), map(RecordPage.parse)); 24 | } 25 | 26 | getOperationTypes(): Observable { 27 | return of(GET_OPERATION_TYPES).pipe( 28 | delay(1), 29 | map(items => items.map(RecordOperationType.parse)) 30 | ); 31 | } 32 | 33 | getActionTypes(): Observable { 34 | return of(GET_ACTION_TYPES).pipe( 35 | delay(1), 36 | map(items => items.map(RecordActionType.parse)) 37 | ); 38 | } 39 | 40 | static get provider(): any { 41 | if (!RecordServiceMock.mock) { 42 | RecordServiceMock.mock = new RecordServiceMock(); 43 | } 44 | return {provide: RecordService, useValue: RecordServiceMock.mock}; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ui/src/app/tests/api/runtime.service.mock.spec.ts: -------------------------------------------------------------------------------- 1 | import {RuntimeService} from '../../shared/api/runtime.service'; 2 | import {Observable, of} from 'rxjs'; 3 | import {RuntimeStreamPage} from '../../shared/model/runtime.model'; 4 | import {GET_RUNTIME} from '../data/runtime'; 5 | import {delay, map} from 'rxjs/operators'; 6 | 7 | export class RuntimeServiceMock { 8 | static mock: RuntimeServiceMock = null; 9 | 10 | constructor() {} 11 | 12 | getRuntime(page: number, size: number): Observable { 13 | return of(GET_RUNTIME).pipe(delay(1), map(RuntimeStreamPage.parse)); 14 | } 15 | 16 | static get provider(): any { 17 | if (!RuntimeServiceMock.mock) { 18 | RuntimeServiceMock.mock = new RuntimeServiceMock(); 19 | } 20 | return {provide: RuntimeService, useValue: RuntimeServiceMock.mock}; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/app/tests/api/schedule.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {Observable, of} from 'rxjs'; 2 | import {Schedule, SchedulePage} from '../../shared/model/schedule.model'; 3 | import {delay, map} from 'rxjs/operators'; 4 | import {GET_SCHEDULE, GET_SCHEDULES} from '../data/schedule'; 5 | import {ScheduleService} from '../../shared/api/schedule.service'; 6 | 7 | export class ScheduleServiceMock { 8 | static mock: ScheduleServiceMock = null; 9 | 10 | getSchedules(search?: string, platform?: string): Observable { 11 | return of(GET_SCHEDULES).pipe(delay(1), map(SchedulePage.parse)); 12 | } 13 | 14 | getSchedule(scheduleName: string): Observable { 15 | return of(GET_SCHEDULE).pipe(delay(1), map(Schedule.parse)); 16 | } 17 | 18 | getSchedulesByTask(task: string, platform: string): Observable { 19 | return of(GET_SCHEDULES).pipe(delay(1), map(SchedulePage.parse)); 20 | } 21 | 22 | createSchedules(schedules: Array): Observable { 23 | return of(schedules); 24 | } 25 | 26 | createSchedule( 27 | schedulerName: string, 28 | task: string, 29 | platform: string, 30 | cronExpression: string, 31 | args: string, 32 | props: string 33 | ): Observable { 34 | return of({}); 35 | } 36 | 37 | destroySchedule(scheduleName: string, platform: string): Observable { 38 | return of({}); 39 | } 40 | 41 | destroySchedules(schedules: Array): Observable { 42 | return of(schedules); 43 | } 44 | 45 | static get provider(): any { 46 | if (!ScheduleServiceMock.mock) { 47 | ScheduleServiceMock.mock = new ScheduleServiceMock(); 48 | } 49 | return {provide: ScheduleService, useValue: ScheduleServiceMock.mock}; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui/src/app/tests/api/security.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {SecurityService} from '../../security/service/security.service'; 2 | import {Observable, of} from 'rxjs'; 3 | import {catchError} from 'rxjs/operators'; 4 | import {ErrorUtils} from '../../shared/support/error.utils'; 5 | import {Security} from '../../shared/model/security.model'; 6 | import {LOAD} from '../data/security'; 7 | 8 | export class SecurityServiceMock { 9 | static mock: SecurityServiceMock = null; 10 | 11 | constructor() {} 12 | 13 | async canAccess(roles: string[]): Promise { 14 | return true; 15 | } 16 | 17 | load(): Observable { 18 | return of(LOAD as Security); 19 | } 20 | 21 | logout(): Observable { 22 | return of(LOAD as Security).pipe(catchError(ErrorUtils.catchError)); 23 | } 24 | 25 | loggedinUser(): Observable { 26 | return of(undefined); 27 | } 28 | 29 | clearLocalSecurity(): void {} 30 | 31 | static get provider(): any { 32 | if (!SecurityServiceMock.mock) { 33 | SecurityServiceMock.mock = new SecurityServiceMock(); 34 | } 35 | return {provide: SecurityService, useValue: SecurityServiceMock.mock}; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/app/tests/data/schedule.ts: -------------------------------------------------------------------------------- 1 | export const GET_SCHEDULES = { 2 | _embedded: { 3 | scheduleInfoResourceList: [ 4 | { 5 | scheduleName: 'foo1', 6 | taskDefinitionName: 'bar1', 7 | platform: 'foo', 8 | scheduleProperties: {'spring.cloud.scheduler.cron.expression': '0 0 0 * 8 1'}, 9 | _links: { 10 | self: { 11 | href: 'http://localhost:4200/tasks/schedules/foo1' 12 | } 13 | } 14 | }, 15 | { 16 | scheduleName: 'foo2', 17 | taskDefinitionName: 'bar2', 18 | platform: 'bar', 19 | scheduleProperties: {'spring.cloud.scheduler.cron.expression': '0 0 0 * 8 1'}, 20 | _links: { 21 | self: { 22 | href: 'http://localhost:4200/tasks/schedules/foo2' 23 | } 24 | } 25 | } 26 | ] 27 | }, 28 | _links: { 29 | self: { 30 | href: 'http://localhost:4200/tasks/schedules?page=0&size=20&sort=NAME,asc' 31 | } 32 | }, 33 | page: { 34 | size: 20, 35 | totalElements: 2, 36 | totalPages: 1, 37 | number: 0 38 | } 39 | }; 40 | 41 | export const GET_SCHEDULE = { 42 | scheduleName: 'foo1', 43 | taskDefinitionName: 'bar1', 44 | scheduleProperties: {'spring.cloud.scheduler.cron.expression': '0 0 0 * 8 1'}, 45 | platform: 'foo', 46 | _links: { 47 | self: { 48 | href: 'http://localhost:4200/tasks/schedules/foo1' 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /ui/src/app/tests/data/security.ts: -------------------------------------------------------------------------------- 1 | export const LOAD = { 2 | authenticationEnabled: false, 3 | authenticated: false, 4 | username: null, 5 | roles: [], 6 | _links: {self: {href: 'http://localhost:4200/security/info'}} 7 | }; 8 | -------------------------------------------------------------------------------- /ui/src/app/tests/service/context.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {Observable, of} from 'rxjs'; 2 | import {ContextModel} from '../../shared/model/context.model'; 3 | import {ContextService} from '../../shared/service/context.service'; 4 | import {initialState} from '../../shared/store/context.reducer'; 5 | 6 | export class ContextServiceMock { 7 | static mock: ContextServiceMock = null; 8 | 9 | constructor() {} 10 | 11 | update(): Observable { 12 | return of({}); 13 | } 14 | 15 | dispatch(): Observable { 16 | return of({}); 17 | } 18 | 19 | getContext(name: string): Observable { 20 | const ctx = initialState.find(x => x.name === name)?.value || []; 21 | return of(ctx); 22 | } 23 | 24 | getContexts(): Observable { 25 | return of(initialState); 26 | } 27 | 28 | updateContext(name: string, contexts: ContextModel[]): Observable { 29 | return of({}); 30 | } 31 | 32 | static get provider(): any { 33 | if (!ContextServiceMock.mock) { 34 | ContextServiceMock.mock = new ContextServiceMock(); 35 | } 36 | return {provide: ContextService, useValue: ContextServiceMock.mock}; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/app/tests/service/group.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {GroupService} from '../../shared/service/group.service'; 2 | 3 | export class GroupServiceMock { 4 | static mock: GroupServiceMock = null; 5 | 6 | constructor() {} 7 | 8 | create(args: any): string { 9 | const key = 'foo'; 10 | return key; 11 | } 12 | 13 | isSimilar(str: string): boolean { 14 | return true; 15 | } 16 | 17 | group(name: string): any { 18 | return ['foo', 'bar']; 19 | } 20 | 21 | static get provider(): any { 22 | if (!GroupServiceMock.mock) { 23 | GroupServiceMock.mock = new GroupServiceMock(); 24 | } 25 | return {provide: GroupService, useValue: GroupServiceMock.mock}; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/app/tests/service/import-export.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {ImportExportService} from '../../shared/service/import-export.service'; 2 | import {Stream} from '../../shared/model/stream.model'; 3 | import {Observable, of} from 'rxjs'; 4 | import {Task} from '../../shared/model/task.model'; 5 | 6 | export class ImportExportServiceMock { 7 | static mock: ImportExportServiceMock = null; 8 | 9 | streamsExport(streams: Stream[]): Observable { 10 | return of({}); 11 | } 12 | 13 | tasksExport(tasks: Task[]): Observable { 14 | return of({}); 15 | } 16 | 17 | streamsImport(file: Blob, optimize: boolean): Observable { 18 | return of([ 19 | { 20 | created: true, 21 | name: 'foo', 22 | dslTest: 'time | log', 23 | error: '', 24 | message: '' 25 | } 26 | ]); 27 | } 28 | 29 | tasksImport(file: Blob, excludeChildren: boolean): Observable { 30 | return of([ 31 | { 32 | created: true, 33 | name: 'foo', 34 | dslTest: 'timestamp', 35 | error: '', 36 | message: '' 37 | } 38 | ]); 39 | } 40 | 41 | static get provider(): any { 42 | if (!ImportExportServiceMock.mock) { 43 | ImportExportServiceMock.mock = new ImportExportServiceMock(); 44 | } 45 | return {provide: ImportExportService, useValue: ImportExportServiceMock.mock}; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/app/tests/service/notification.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {NotificationService} from '../../shared/service/notification.service'; 2 | 3 | export class NotificationServiceMock { 4 | static mock: NotificationServiceMock = null; 5 | 6 | successNotifications: any = []; 7 | 8 | errorNotification: any = []; 9 | 10 | warningNotification: any = []; 11 | 12 | infoNotification: any = []; 13 | 14 | constructor() {} 15 | 16 | success(title: string, message?: string): any { 17 | this.successNotifications.push({title, message}); 18 | } 19 | 20 | error(title: string, message?: string): any { 21 | this.errorNotification.push({title, message}); 22 | } 23 | 24 | info(title: string, message?: string): any { 25 | this.infoNotification.push({title, message}); 26 | } 27 | 28 | warning(title: string, message?: string): any { 29 | this.warningNotification.push({title, message}); 30 | } 31 | 32 | clearAll(): void { 33 | this.successNotifications = []; 34 | this.errorNotification = []; 35 | } 36 | 37 | static get provider(): any { 38 | if (!NotificationServiceMock.mock) { 39 | NotificationServiceMock.mock = new NotificationServiceMock(); 40 | } 41 | return {provide: NotificationService, useValue: NotificationServiceMock.mock}; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ui/src/app/tests/service/settings.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {SettingsService} from '../../settings/settings.service'; 2 | import {Observable, of} from 'rxjs'; 3 | import {SettingModel} from '../../shared/model/setting.model'; 4 | 5 | export class SettingsServiceMock { 6 | static mock: SettingsServiceMock = null; 7 | 8 | static DATA = [ 9 | {name: 'theme-active', value: 'dark'}, 10 | {name: 'results-per-page', value: '20'} 11 | ]; 12 | 13 | constructor() {} 14 | 15 | load(): Observable { 16 | return of(SettingsServiceMock.DATA); 17 | } 18 | 19 | update(): Observable { 20 | return of({}); 21 | } 22 | 23 | dispatch(): Observable { 24 | return of({}); 25 | } 26 | 27 | themeActiveSetting(): Observable { 28 | return of('dark'); 29 | } 30 | 31 | getContext(name: string): Observable { 32 | const ctx = SettingsServiceMock.DATA.find(x => x.name === name)?.value || []; 33 | return of(ctx); 34 | } 35 | 36 | getSettings(): Observable { 37 | return of(SettingsServiceMock.DATA); 38 | } 39 | 40 | updateContext(name: string, settings: SettingModel[]): Observable { 41 | return of({}); 42 | } 43 | 44 | static get provider(): any { 45 | if (!SettingsServiceMock.mock) { 46 | SettingsServiceMock.mock = new SettingsServiceMock(); 47 | } 48 | return {provide: SettingsService, useValue: SettingsServiceMock.mock}; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ui/src/app/tests/service/stream-deploy.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {StreamDeployService} from '../../streams/streams/stream-deploy.service'; 2 | import {Observable, of} from 'rxjs'; 3 | import {Stream, StreamDeployConfig} from '../../shared/model/stream.model'; 4 | import {GET_STREAM} from '../data/stream'; 5 | import {APP_DETAILS, CONFIG} from '../data/stream-deploy'; 6 | import {ApplicationType} from '../../shared/model/app.model'; 7 | 8 | export class StreamDeployServiceMock { 9 | static mock: any = null; 10 | 11 | deploymentProperties(name: string): Observable { 12 | const properties = []; 13 | const ignoreProperties = []; 14 | const stream = Stream.parse(GET_STREAM); 15 | return of({ 16 | properties, 17 | ignoreProperties: [...properties, ...ignoreProperties], 18 | stream 19 | }); 20 | } 21 | 22 | config(id: string): Observable { 23 | const config = new StreamDeployConfig(); 24 | config.id = CONFIG.id; 25 | config.platform = CONFIG.platform; 26 | config.deployers = CONFIG.deployers; 27 | config.apps = CONFIG.apps; 28 | return of(config); 29 | } 30 | 31 | appDetails(type: ApplicationType, name: string, version: string): Observable { 32 | return of(APP_DETAILS); 33 | } 34 | 35 | static get provider(): any { 36 | if (!StreamDeployServiceMock.mock) { 37 | StreamDeployServiceMock.mock = new StreamDeployServiceMock(); 38 | } 39 | return {provide: StreamDeployService, useValue: StreamDeployServiceMock.mock}; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/app/tests/service/task-tools.service.mock.ts: -------------------------------------------------------------------------------- 1 | import {Observable, of} from 'rxjs'; 2 | import {ToolsService} from '../../flo/task/tools.service'; 3 | import {Graph, TaskConversion} from '../../flo/task/model/models'; 4 | 5 | export class ToolsServiceMock extends ToolsService { 6 | static mock: ToolsServiceMock = null; 7 | 8 | constructor() { 9 | super(null); 10 | } 11 | 12 | parseTaskTextToGraph(dsl: string, name: string = 'unknown'): Observable { 13 | return of(new TaskConversion(dsl, [])); 14 | } 15 | 16 | convertTaskGraphToText(graph: Graph): Observable { 17 | return of(new TaskConversion('', [], graph)); 18 | } 19 | 20 | static get provider(): any { 21 | if (!ToolsServiceMock.mock) { 22 | ToolsServiceMock.mock = new ToolsServiceMock(); 23 | } 24 | return {provide: ToolsService, useValue: ToolsServiceMock.mock}; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/url-utilities.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class UrlUtilities { 7 | static dashboard = 'dashboard/'; 8 | static assets = 'assets/'; 9 | constructor() {} 10 | 11 | public static calculateBaseApiUrl() { 12 | // Remove dashboard from base url 13 | return this.calculateBaseUrl().replace('/' + this.dashboard, '/'); 14 | } 15 | 16 | public static calculateAssetUrl() { 17 | return this.calculateBaseUrl() + this.assets; 18 | } 19 | 20 | public static calculateBaseUrl() { 21 | // Remove anything like index.html 22 | let path: string = window.location.pathname.replace(/[^/]*$/, ''); 23 | // Check if the path ends with / 24 | path += path.endsWith('/') ? '' : '/'; 25 | return path; 26 | } 27 | 28 | public static fixReverseProxyUrl(url: string, active: boolean) { 29 | if (!active) { 30 | return url; 31 | } 32 | try { 33 | const urlToFix: URL = new URL(url); 34 | const baseUrl: URL = new URL(window.location.href); 35 | urlToFix.host = baseUrl.host; 36 | urlToFix.protocol = baseUrl.protocol; 37 | urlToFix.port = baseUrl.port; 38 | return urlToFix.href; 39 | } catch (_) { 40 | return url; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ui/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/ui/src/assets/.gitkeep -------------------------------------------------------------------------------- /ui/src/assets/img/api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/ui/src/assets/img/api.png -------------------------------------------------------------------------------- /ui/src/assets/img/app.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /ui/src/assets/img/chevron-down-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/assets/img/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/assets/img/chevron-left-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/assets/img/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/assets/img/cog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /ui/src/assets/img/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/src/assets/img/documentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/ui/src/assets/img/documentation.png -------------------------------------------------------------------------------- /ui/src/assets/img/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /ui/src/assets/img/forum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/ui/src/assets/img/forum.png -------------------------------------------------------------------------------- /ui/src/assets/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/ui/src/assets/img/github.png -------------------------------------------------------------------------------- /ui/src/assets/img/processor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/assets/img/project-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/ui/src/assets/img/project-page.png -------------------------------------------------------------------------------- /ui/src/assets/img/shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/ui/src/assets/img/shell.png -------------------------------------------------------------------------------- /ui/src/assets/img/sink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /ui/src/assets/img/source.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /ui/src/assets/img/tap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ui/src/assets/img/tracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/ui/src/assets/img/tracker.png -------------------------------------------------------------------------------- /ui/src/assets/img/unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /ui/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /ui/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-attic/spring-cloud-dataflow-ui/c31733bc9be66e9014690a5c0b9682b19518368e/ui/src/favicon.ico -------------------------------------------------------------------------------- /ui/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-edge-launcher'), 12 | require('karma-firefox-launcher'), 13 | require('karma-safari-launcher'), 14 | require('karma-jasmine-html-reporter'), 15 | require('karma-spec-reporter'), 16 | require('karma-coverage'), 17 | require('@angular-devkit/build-angular/plugins/karma') 18 | ], 19 | client:{ 20 | clearContext: false // leave Jasmine Spec Runner output visible in browser 21 | }, 22 | 23 | // optionally, configure the reporter 24 | coverageReporter: { 25 | type : 'html', 26 | dir : '../coverage/' 27 | }, 28 | reporters: ['spec', 'progress', 'coverage', 'kjhtml'], 29 | specReporter: { 30 | maxLogLines: 5, 31 | suppressErrorSummary: false, 32 | suppressFailed: false, 33 | suppressPassed: false, 34 | suppressSkipped: false 35 | }, 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['ChromeCustom'], 41 | customLaunchers: { 42 | ChromeCustom: { 43 | base: 'ChromeHeadless', 44 | flags: [ 45 | '--headless', 46 | '--disable-web-security', 47 | '--disable-gpu', 48 | '--no-sandbox' 49 | ] 50 | } 51 | }, 52 | singleRun: false, 53 | browserNoActivityTimeout: 100000 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /ui/src/logout-success-oauth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spring Cloud Data Flow 6 | 7 | 8 | 28 | 29 | 30 |
31 |
32 |
33 |

You have successfully logged out from the Spring Cloud Data Flow Dashboard.

34 |

35 | Please keep in mind that you are most likely still logged in with your oauth provider. Therefore, going to 36 | the main Dashboard URL will log you back in, unless you also log out from your oauth provider. 37 |

38 |
39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | 4 | import {AppModule} from './app/app.module'; 5 | import {environment} from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.log(err)); 14 | -------------------------------------------------------------------------------- /ui/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | @import '../node_modules/spring-flo/flo.css'; 4 | @import 'app/flo/shared/flo.scss'; 5 | @import '../node_modules/tippy.js/dist/tippy.css'; 6 | @import './scss/styles'; 7 | -------------------------------------------------------------------------------- /ui/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import {getTestBed} from '@angular/core/testing'; 5 | import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; 6 | 7 | // First, initialize the Angular testing environment. 8 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 9 | teardown: { destroyAfterEach: false } 10 | }); 11 | -------------------------------------------------------------------------------- /ui/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck": true, 5 | "outDir": "../out-tsc/app", 6 | "types": ["node"] 7 | }, 8 | "files": ["main.ts", "polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck": true, 5 | "outDir": "../out-tsc/spec", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "importHelpers": true, 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "es2020", 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "resolveJsonModule": true, 14 | "esModuleInterop": true, 15 | "target": "ES2022", 16 | "typeRoots": [ 17 | "node_modules/@types" 18 | ], 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ], 23 | "useDefineForClassFields": false 24 | } 25 | } 26 | --------------------------------------------------------------------------------