diff --git a/.gitignore b/.gitignore index f3e139f0c3..01c8401c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # compiled output /dist +.dist /tmp /out-tsc /out diff --git a/README.md b/README.md index ac8a59471a..49c6cef10e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Stratos is an Open Source Web-based UI (Console) for managing Cloud Foundry. It allows users and administrators to both manage applications running in the Cloud Foundry cluster and perform cluster management tasks. -![Stratos Application view](docs/images/screenshots/app-wall.png) + ## Deploying Stratos @@ -35,8 +35,8 @@ You can access the UI on `https://localhost:4443` ## Project Planning We use [ZenHub](https://zenhub.com) for project planning. Feel free to head over to the [Boards](https://github.com/SUSE/stratos#boards) -tab and have a look through our pipelines and milestones. Please note in order to view the ZenHub Boards tab you will need the [ZenHub -browser extension](https://www.zenhub.com/extension). +tab and have a look through our pipelines and milestones. Please note in order to view the Github ZenHub Boards tab you will need the [ZenHub +browser extension](https://www.zenhub.com/extension). Alternatively, to view the planning board without the extension visit our [ZenHub Project Page](https://app.zenhub.com/workspace/o/cloudfoundry-incubator/stratos/boards) ## Further Reading diff --git a/deploy/cloud-foundry/README.md b/deploy/cloud-foundry/README.md index dc8639f020..767e16e919 100644 --- a/deploy/cloud-foundry/README.md +++ b/deploy/cloud-foundry/README.md @@ -2,11 +2,11 @@ ## Deployment Steps -The quickest way to install Stratos UI is to deploy it as a Cloud Foundry application. To do so, clone the `stratos-ui` repository, cd into the newly cloned repository and push to Cloud Foundry. This can be done with: +The quickest way to install Stratos UI is to deploy it as a Cloud Foundry application. To do so, clone the `stratos` repository, cd into the newly cloned repository and push to Cloud Foundry. This can be done with: ``` -git clone https://github.com/SUSE/stratos-ui.git -cd stratos-ui +git clone https://github.com/cloudfoundry-incubator/stratos +cd stratos cf push ``` @@ -27,27 +27,8 @@ Note: 1. You need the cf CLI command line tool installed and available on the path. 2. You need to have configured the cf cli to point to your Cloud Foundry cluster, to be authenticated with your credentials and to be targeted at the organization and space where you want the console application be created. -3. You may need to configure Application Security Groups on your Cloud Foundry Cluster in order that Stratos UI can communicate with the Cloud Foundry API. See [below](#application-security-groups) for more information. -4. The Stratos UI Console will automatically detect the API endpoint for your Cloud Foundry. To do so, it relies on the `cf_api_url` value inside the `VCAP_APPLICATION` environment variable. If this is not provided by your Cloud Foundry platform, then you must manually update the application manifest as described [below](#console-fails-to-start). - -## Enable Endpoints Dashboard to register additional Cloud Foundry endpoints - -To enable the dashboard add the environment variable 'FORCE_ENDPOINT_DASHBOARD' to the manifest before the call to 'cf push' is made. For example - - ``` - applications: - - name: console - memory: 256M - disk_quota: 256M - host: console - timeout: 180 - buildpack: https://github.com/SUSE/stratos-buildpack - health-check-type: port - env: - FORCE_ENDPOINT_DASHBOARD: true - ``` - ->**NOTE** This step, on it's own, is meant for demonstration purposes only. Registered endpoints will be lost if the app is restarted and each app instance will have it's own lists. To remove these caveats see the section 'Associate Cloud Foundry database service' below. +3. You may need to configure Application Security Groups on your Cloud Foundry Cluster in order that Stratos can communicate with the Cloud Foundry API. See [below](#application-security-groups) for more information. +4. The Stratos Console will automatically detect the API endpoint for your Cloud Foundry. To do so, it relies on the `cf_api_url` value inside the `VCAP_APPLICATION` environment variable. If this is not provided by your Cloud Foundry platform, then you must manually update the application manifest as described [below](#console-fails-to-start). ## Associate Cloud Foundry database service Follow instructions [here](db-migration/README.md). @@ -149,7 +130,7 @@ applications: disk_quota: 256M host: console timeout: 180 - buildpack: https://github.com/SUSE/stratos-buildpack + buildpack: https://github.com/cloudfoundry-incubator/stratos-buildpack health-check-type: port env: CF_API_URL: https://<>> @@ -166,7 +147,7 @@ applications: disk_quota: 256M host: console timeout: 180 - buildpack: https://github.com/SUSE/stratos-buildpack + buildpack: https://github.com/cloudfoundry-incubator/stratos-buildpack health-check-type: port env: CF_API_FORCE_SECURE: true diff --git a/docs/developers-guide.md b/docs/developers-guide.md index d3528a0435..37892d4540 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -116,5 +116,53 @@ Run `npm run e2e` to execute the end-to-end tests via [Protractor](http://www.pr ## Backend Development The backend (more informally called the portal-proxy or 'pp' for short) is still to be ported over from V1 of -[Stratos](https://github.com/SUSE/stratos-ui). Once that's completed come back and check out this section for instructions on how to +[Stratos](https://github.com/cloudfoundry-incubator/stratos). Once that's completed come back and check out this section for instructions on how to make changes to it. + +WIP + +### Getting started + +The portal-proxy is the back-end for the Console UI. It is written in Go. + +### Automatically register and connect to an existing endpoint +To automatically register a Cloud Foundry add the environment variable below + +> **Note** On log in the console will also attempt to auto-connect to the cloud foundry using + the username/password provided. + +``` +AUTO_REG_CF_URL= +``` + +This env var can be set in `outputs/config.properties` if running the backend locally in the host machine, `./deploy/proxy.env` if running in docker-compose or `./manifest` if in cf push. + +> **NOTE** WIP Instructions! + +#### Introduction +* Golang +* Dependency Management (Glide) + +#### Dependencies +* go + * GOPATH, GOBIN env vars set +* glide +* UAA instance + +#### Running portal-proxy in a container +* Follow instructions in the deploy/docker-compose docs +* To apply changes (build and update docker image) simply run `deploy/tools/restart_proxy.sh` + +#### Running "like a dev" + +1. Set up developer certs + - Execute `deploy/tools/generate_cert.sh` + - Copy `portal-proxy-output/dev-certs` to `./` +1. Update `build/dev_config.json` with `"localDevBuild": true` +1. Run `gulp local-dev-build` +1. cd ./outputs +1. Run `gulp build-backend` +1. Update `config.propeties` and ensure that.. + - the UAA points to a valid instance + - the `CONSOLE_CLIENT` and `CONSOLE_ADMIN_SCOPE` are valid in the UAA instance +1. Run `portal-proxy` diff --git a/docs/development.md b/docs/development.md deleted file mode 100644 index 40cba69e92..0000000000 --- a/docs/development.md +++ /dev/null @@ -1,284 +0,0 @@ -# Developing the Stratos Console - -> **Note:** This document is work in progress. - - -The Stratos Console UI provides a single comprehensive and ubiquitous user -experience for: discovering, composing, developing and managing Cloud Native -workloads which are hosted in a myriad of: public, managed and private -cloud/compute providers. - -1. [Working on the front-end component](#working-on-the-front-end-component) -2. [Working on the back-end component](#working-on-the-backend-component) -3. [Testing](#testing) - -## Dependencies - -### Node -Please check the `engine` entry of the package.json file for the required node version. - > **Note:** To manage node versions we recommend using [nvm](https://github.com/creationix/nvm) - -## Components - -The top-level repository contains three folders worth calling out: - -Folder | Description --------|------------ -build | Contains build scripts (mainly gulp-based) for both the backend and frontend -components | Contains the source code for the Console -deploy | Contains scripts and artifacts for deploying the Console. - -The Console code can be found in the [components](../components) folder. The following components are currently included: - -Component Name | Description ----------------|------------ -about-app | Provides an "about" view for the Console -app-core | Contains core functionality, such as login and navigation -app-framework | Providers a set of UI widgets and utilities -app-theme | Style information for the app-framework -cloud-foundry | All Cloud Foundry specific code -cloud-foundry-hosting | Specific component used for hosting the Console as an app in a single Cloud Foundry -endpoints-dashboard | Manage Console endpoints, specifically Cloud Foundry's. The inclusion allows additional Cloud Foundry endpoints to be added -suse-branding | Overrides styles to show the Console with SUSE branding - -### Component architecture - -Components can include both frontend and backend code. The source code for these sits together under a single component folder. - -The Console Frontend is written using AngularJS and the backend written in Go. - -The Console build determines the location of the frontend and backend code via a configuration file -named `[component name].component.json`. -When no frontend configuration is found in *.component.json it is assumed the component is purely for the frontend and that its source -code sites directly in the component folder, father than a `frontend` subfolder. - -### Including your own components -Other components can be included to add additional items to the navigation bar and their associated content. This can include additional types of 'endpoints' (an existing endpoint for example is Cloud Foundry). Instructions on how to carry out this will be added at a later date. - -## Working on the front end component - -### Source Code Structure - -The frontend code is split into component directories as listed above. -The standard set of components that exist in the console contain functionality -to manage Cloud Foundry instances and their applications. - -The frontend code is usually found within a `frontend` folder and contains -a structure such as that in app-core/frontend component, for example: -``` -|-- frontend -| |-- assets -| |-- i18n -| | -- en -| |-- src -| | |-- api -| | |-- model -| | |-- utils -| | |-- view -| |-- test -| `-- index.html -|-- app-core.component.json -`-- bower.json -``` - -Directory | Contains -----------|------------ -assets | Any images required by the front end -i18n | Internationalization strings per locale. By default the console contains English (US) -src | Javascript, html and scss related to the component -test | Unit tests for the component - -> **Note:** The bower.json is in the root of the component - -### Style Sheets -The frontend defines styles in SCSS which is converted to CSS at build time. -Each component is responsible for specifying it's root scss as a 'main' file -in it's bower.json. From this all other -component scss are gathered. - -### Build Process -The build process uses gulp, see the the root gulpfile.js. Below is a list -of important gulp tasks. - -Gulp task name | Description -----------|------------ -clean | Removes the dist folder -dev | Executes a developer build and serves the console via browser sync -run | Executes a production build and serves the console via express -lint | Executes linting via eslint. See ./.eslintrc for rules - -> **Note:** When using the `dev` task, web sockets do not get forwarded, so log streaming and ssh access will not work - use the `run` task to test these. - -Some tasks can be accessed via npm, by running `npm script target` along with additional test -functionality: - -NPM script name | Description -----------------|------------ -lint | Same as gulp lint -coverage | Executes both unit and e2e tests and provides a combined coverage report in `./out/coverage-report` -gate-check | Executes lint and unit tests, very handy to use before creating a PR -e2e | Executes end to end tests via protractor, also handy to use before creating a PR. Screenshots of the console for each failure can be found in ./out/e2e-failures -test | Executes unit tests - - -### Run the frontend via gulp - -#### Requirements -The Console backend must be up and contactable by the developers machine. -This can be achieved via any of the methods -described in the [deploy](../deploy/README.md) instructions. - -#### Configuration -The Console frontend must know the address of the backend. This can be -set by creating the file ./build/dev_config.json -with contents such as -``` -{ - "pp": "/pp" -} -``` - -For example, if the console was deployed and accessible via `https://localhost` - -the following configuration should be used -``` -{ - "pp": "https://localhost/pp" -} -``` -Run the following commands to install the dependencies -``` -$ npm install -$ bower install - -``` - -#### Run -To run the frontend with bits as if it were production (uses minified resources) execute ... -``` -$ gulp run -``` - -To run the frontend in development mode (uses non-minified resources and serves via browsersync) execute ... -``` -$ gulp dev -``` - -In both cases the console should be available via https://localhost:3100 - -> **Note:** If you see the following error when running 'gulp dev' you may need to increase your OS ulimit. -``` -Error: ENFILE: file table overflow, scandir ; - at Error (native) --bash: /dev/null: Too many open files in system -``` - -### Linting -We use eslint to executing linting. To run these execute... -``` -$ gulp lint -``` - - -### Creating a successful Pull Request in github -For every new pull request, or commit to an existing request, the CI will -run a build against the requests HEAD. Before creating a PR or pushing to -one please ensure the following two requests execute successfully - -``` -$ npm run gate-check -``` -(lint + unit tests) - -``` -$ npm run e2e -``` -(e2e tests) - -## Working on the backend component - -### Getting started - -The portal-proxy is the back-end for the Console UI. It is written in Go. - -### Automatically register and connect to an existing endpoint -To automatically register a Cloud Foundry add the environment variable below - -> **Note** On log in the console will also attempt to auto-connect to the cloud foundry using - the username/password provided. - -``` -AUTO_REG_CF_URL= -``` - -This env var can be set in `outputs/config.properties` if running the backend locally in the host machine, `./deploy/proxy.env` if running in docker-compose or `./manifest` if in cf push. - -> **NOTE** WIP Instructions! - -#### Introduction -* Golang -* Dependency Management (Glide) - -#### Dependencies -* go - * GOPATH, GOBIN env vars set -* glide -* UAA instance - -#### Running portal-proxy in a container -* Follow instructions in the deploy/docker-compose docs -* To apply changes (build and update docker image) simply run `deploy/tools/restart_proxy.sh` - -#### Running "like a dev" - -1. Set up developer certs - - Execute `deploy/tools/generate_cert.sh` - - Copy `portal-proxy-output/dev-certs` to `./` -1. Update `build/dev_config.json` with `"localDevBuild": true` -1. Run `gulp local-dev-build` -1. cd ./outputs -1. Run `gulp build-backend` -1. Update `config.propeties` and ensure that.. - - the UAA points to a valid instance - - the `CONSOLE_CLIENT` and `CONSOLE_ADMIN_SCOPE` are valid in the UAA instance -1. Run `portal-proxy` - -#### Tests - -##### Unit Testing - - -## Testing - -### Front End Unit Tests -Unit test are written via jasmine and executed in karma. To run these execute... -``` -$ npm test -``` - -### End to End Tests -To run e2e tests a cloud foundry with specific orgs, spaces and users is required. - -To set this up -1. Ensure the cf cli tool is installed. See https://github.com/cloudfoundry/cli -2. cf tool has targeted the cf instance and logged in (cf api, cf login) -3. Execute the following script to set up the SUSE org, dev space and e2e user - -**NOTE** This will also create an application which will continually output log statements. This will be used to test - the log reader. -``` -$ ./test/e2e/config-cf.sh -``` -4. Copy ./build/secrets.json.sample to ./build/secrets.json and update cloudFoundry url and cf admin username/password -5. Execute the tests via... -``` -$ npm run e2e -``` - -#### Continuous Integration - -Pull request submitted to the stratos-ui project will run through -frontend unit tests, backend unit tests and integration tests. The -concourse server which executes these is currently not available -externally. The result however can still be seen by the usual -indications posted by github to the PR's page. diff --git a/docs/features.md b/docs/features.md index d1d3194d88..596729caa3 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,5 +1,7 @@ # Features +> **NOTE** This branch is a work-in-progress Angular 2.x version of Stratos. For the current Angular 1 based version see [https://github.com/cloudfoundry-incubator/stratos/tree/master](https://github.com/cloudfoundry-incubator/stratos/tree/master). This version is at an early development stage and we welcome feedback, input and contributions. This version does not currently have feature parity with the Angular 1 version specified below - see the [Development Roadmap](docs/roadmap.md) for more information. + Stratos provides the feature set outline below. Some of these are illustrated in the [Screenshots gallery](images/screenshots/README.md). * Authentication diff --git a/docs/i18n.md b/docs/i18n.md index 3f7bb092d4..0fc6e721e9 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -1,92 +1,3 @@ # Internationalization (i18n) -Stratos supports i18n via Angular's [$translate service](https://github.com/angular-translate/angular-translate). Dates and times are handled by [Moment](https://momentjs.com/)'s i18n process. -The locale shown will at first be selected from the browser. Any change of locale by the user will be retained within the browser's session. - -The source for all translations can be found in the relevant component's i18n folder. For example: - -``` -|-- i18n -| |-- en_US -``` - -or - -``` -|-- frontend -| |-- i18n -| |-- en_US -``` - -Within each locale will be one or more json files. All json files within a locale will be combined and served as i18n/locale-`locale name`.json, for example locale-en_US.json. - -## Supported Locales -The Stratos UI Console is currently provided with a single locale - US English (en-US). Other locales can easily be added as needed. - -## Localization (i10n) -To add a new locale run through the following steps. - -1. Copy the i18n/en_US directory in every plugin and rename it for the required locale. For instance here en_GB is added -``` -|-- frontend -| |-- i18n -| |-- en_US -| |-- en_GB -``` -2. Add the new locale to the selection of locales to choose. For example -* Open the file `components/app-core/frontend/i18n/en_US/locales.json` -* Add the new locale -``` -{ - "locales": { - "locales": "en_US,fr,en_GB", - "en_US": "English (US)", - "en_GB": "English (UK)" - } -} -``` -* Repeat for all versions of the 'locales' object in all other locale files. - -3. Start the stratos ui and the new locale should be visible in the change language drop down. -4. Go through all the new locale's json files and update with the required translations. - -## i10n Useful Information -* To find a string in HTML/JS flatten the property path. For example -``` -{ - "login": { - "timeout": { - "prompt": "Are you still there?" - } - } -} -``` -Becomes -``` -login.timeout.prompt -``` -* If any string is missing it's translation it will appear as a warning in the browser's console output -* Some strings use substitution. For example consider the following -``` -{ - "login": { - "login": "Login", - "console": "@:product.console", - "title": "[[@:product.name]] [[@:product.version]]", - "welcome": "Use the [[@:product.console]] to develop, compose, and manage Cloud Native workloads.", - } -} -``` -login.console will be replaced with the entry for product.console. If a string contains more than the direct replacement then the [[@:]] notation should be used. -* Some strings can contain dynamic content. For exammple -``` -{ - "login": { - "timeout": { - "notice": "You have been inactive for a while. For your protection, we will automatically log you out in {{timeout}}", - } - } -} -``` -login.timeout.notice contains a countdown in seconds until the user is automatically logged out '{{timeout}}'. The text within the curly brackets should not be changed but it's location within the string is for the translator to decide - \ No newline at end of file +WIP diff --git a/src/frontend/app/core/entity-service.ts b/src/frontend/app/core/entity-service.ts index be586cf5e3..44196e6777 100644 --- a/src/frontend/app/core/entity-service.ts +++ b/src/frontend/app/core/entity-service.ts @@ -3,7 +3,7 @@ import { Store } from '@ngrx/store'; import { denormalize, Schema } from 'normalizr'; import { tag } from 'rxjs-spy/operators/tag'; import { interval } from 'rxjs/observable/interval'; -import { filter, map, publishReplay, refCount, shareReplay, tap, withLatestFrom, share } from 'rxjs/operators'; +import { filter, map, shareReplay, tap, withLatestFrom, share } from 'rxjs/operators'; import { Observable } from 'rxjs/Rx'; import { AppState } from '../store/app-state'; diff --git a/src/frontend/app/core/utils.service.ts b/src/frontend/app/core/utils.service.ts index bcb348e772..a83d6ed7d4 100644 --- a/src/frontend/app/core/utils.service.ts +++ b/src/frontend/app/core/utils.service.ts @@ -12,41 +12,41 @@ export class UtilsService { * */ public urlValidationExpression = - '^' + - // protocol identifier - 'http(s)?://' + - // user:pass authentication - '(?:\\S+(?::\\S*)?@)?' + - '(?:' + - // IP address exclusion - // private & local networks - '(?!(?:10|127)(?:\\.\\d{1,3}){3})' + - '(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' + - '(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' + - // IP address dotted notation octets - // excludes loopback network 0.0.0.0 - // excludes reserved space >= 224.0.0.0 - // excludes network & broadcast addresses - // (first & last IP address of each class) - '(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' + - '(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' + - '(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' + - '|' + - // host name - '(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' + - // domain name - '(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' + - // TLD identifier - '(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' + - // TLD may end with dot - '\\.?' + - ')' + - // port number - '(?::\\d{2,5})?' + - // resource path - '(?:[/?#]\\S*)?' + - '$' - ; + '^' + + // protocol identifier + 'http(s)?://' + + // user:pass authentication + '(?:\\S+(?::\\S*)?@)?' + + '(?:' + + // IP address exclusion + // private & local networks + '(?!(?:10|127)(?:\\.\\d{1,3}){3})' + + '(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' + + '(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' + + // IP address dotted notation octets + // excludes loopback network 0.0.0.0 + // excludes reserved space >= 224.0.0.0 + // excludes network & broadcast addresses + // (first & last IP address of each class) + '(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' + + '(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' + + '(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' + + '|' + + // host name + '(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' + + // domain name + '(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' + + // TLD identifier + '(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' + + // TLD may end with dot + '\\.?' + + ')' + + // port number + '(?::\\d{2,5})?' + + // resource path + '(?:[/?#]\\S*)?' + + '$' + ; constructor() { } @@ -146,14 +146,14 @@ export class UtilsService { const hours = Math.floor(uptime / 3600); uptime = uptime % 3600; const minutes = Math.floor(uptime / 60); - const seconds = uptime % 60; + const seconds = uptime % 60; return ( this.formatPart(days, 'd', 'd') + this.formatPart(hours, 'h', 'h') + this.formatPart(minutes, 'm', 'm') + this.formatPart(seconds, 's', 's') - .trim() + .trim() ); } diff --git a/src/frontend/app/features/applications/application-monitor.service.ts b/src/frontend/app/features/applications/application-monitor.service.ts index 9b41ad0419..5088fa1e81 100644 --- a/src/frontend/app/features/applications/application-monitor.service.ts +++ b/src/frontend/app/features/applications/application-monitor.service.ts @@ -63,7 +63,7 @@ export class AppMonitorState { @Injectable() export class ApplicationMonitorService { - appMonitor$: Observable; + appMonitor$: Observable; constructor( private applicationService: ApplicationService, diff --git a/src/frontend/app/features/applications/application.service.ts b/src/frontend/app/features/applications/application.service.ts index 33d50f7ade..7db2534a15 100644 --- a/src/frontend/app/features/applications/application.service.ts +++ b/src/frontend/app/features/applications/application.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; -import { map } from 'rxjs/operators'; +import { map, mergeMap } from 'rxjs/operators'; import { EntityService } from '../../core/entity-service'; import { EntityServiceFactory } from '../../core/entity-service-factory.service'; @@ -18,7 +18,6 @@ import { } from '../../store/actions/app-metadata.actions'; import { GetApplication, UpdateApplication, UpdateExistingApplication } from '../../store/actions/application.actions'; import { ApplicationSchema } from '../../store/actions/application.actions'; -import { SpaceSchema } from '../../store/actions/space.action'; import { AppState } from '../../store/app-state'; import { ActionState } from '../../store/reducers/api-request-reducer/types'; import { selectEntity } from '../../store/selectors/api.selectors'; @@ -46,6 +45,7 @@ import { } from './application/application-tabs-base/tabs/build-tab/application-env-vars.service'; import { getRoute, isTCPRoute } from './routes/routes.helper'; import { PaginationMonitor } from '../../shared/monitors/pagination-monitor'; +import { spaceSchemaKey, organisationSchemaKey } from '../../store/actions/action-types'; export interface ApplicationData { fetching: boolean; @@ -57,7 +57,6 @@ export interface ApplicationData { @Injectable() export class ApplicationService { - applicationInstanceState$: Observable; private appEntityService: EntityService; private appSummaryEntityService: EntityService; @@ -157,9 +156,13 @@ export class ApplicationService { .filter(entityInfo => entityInfo.entity && entityInfo.entity.entity && entityInfo.entity.entity.cfGuid) .map(entityInfo => entityInfo.entity.entity) .do(app => { - this.appSpace$ = this.store.select(selectEntity(SpaceSchema.key, app.space_guid)); - // See https://github.com/SUSE/stratos/issues/158 (Failing to populate entity store with a space's org) - this.appOrg$ = this.store.select(selectEntity(SpaceSchema.key, app.space_guid)).map(space => space.entity.organization); + this.appSpace$ = this.store.select(selectEntity(spaceSchemaKey, app.space_guid)); + this.appOrg$ = this.appSpace$.pipe( + map(space => space.entity.organization_guid), + mergeMap(orgGuid => { + return this.store.select(selectEntity(organisationSchemaKey, orgGuid)); + }) + ); }) .take(1) .subscribe(); @@ -191,26 +194,15 @@ export class ApplicationService { AppStatSchema ) }, true); + // This will fail to fetch the app stats if the current app is not running but we're + // willing to do this to speed up the initial fetch for a running application. + this.appStats$ = appStats.entities$; - this.appStats$ = this.waitForAppEntity$ - .filter(ai => ai && ai.entity && ai.entity.entity) - .switchMap(ai => { - if (ai.entity.entity.state === 'STARTED') { - return appStats.entities$; - } else { - return Observable.of(new Array>()); - } - }); - - this.appStatsFetching$ = this.waitForAppEntity$ - .filter(ai => ai && ai.entity && ai.entity.entity && ai.entity.entity.state === 'STARTED') - .switchMap(ai => { - return appStats.pagination$; - }).shareReplay(1); + this.appStatsFetching$ = appStats.pagination$.shareReplay(1); this.application$ = this.waitForAppEntity$ .combineLatest( - this.store.select(endpointEntitiesSelector), + this.store.select(endpointEntitiesSelector), ) .filter(([{ entity, entityRequestInfo }, endpoints]: [EntityInfo, any]) => { return entity && entity.entity && entity.entity.cfGuid; @@ -226,15 +218,9 @@ export class ApplicationService { }).shareReplay(1); this.applicationState$ = this.waitForAppEntity$ - .withLatestFrom(this.appStats$) + .combineLatest(this.appStats$.startWith(null)) .map(([appInfo, appStatsArray]: [EntityInfo, APIResource[]]) => { - return this.appStateService.get(appInfo.entity.entity, appStatsArray.map(apiResource => apiResource.entity)); - }).shareReplay(1); - - this.applicationInstanceState$ = this.waitForAppEntity$ - .withLatestFrom(this.appStats$) - .switchMap(([appInfo, appStatsArray]: [EntityInfo, APIResource[]]) => { - return ApplicationService.getApplicationState(this.store, this.appStateService, appInfo.entity.entity, this.appGuid, this.cfGuid); + return this.appStateService.get(appInfo.entity.entity, appStatsArray ? appStatsArray.map(apiResource => apiResource.entity) : null); }).shareReplay(1); this.applicationStratProject$ = this.appEnvVars.entities$.map(applicationEnvVars => { @@ -246,11 +232,8 @@ export class ApplicationService { /** * An observable based on the core application entity */ - this.isFetchingApp$ = Observable.combineLatest( - this.app$.map(ei => ei.entityRequestInfo.fetching), - this.appSummary$.map(as => as.entityRequestInfo.fetching) - ) - .map((fetching) => fetching[0] || fetching[1]).shareReplay(1); + this.isFetchingApp$ = this.appEntityService.isFetchingEntity$; + this.isUpdatingApp$ = this.app$.map(a => { diff --git a/src/frontend/app/features/applications/application/application-tabs-base/application-tabs-base.component.html b/src/frontend/app/features/applications/application/application-tabs-base/application-tabs-base.component.html index cf6f35b141..0a18f748b6 100644 --- a/src/frontend/app/features/applications/application/application-tabs-base/application-tabs-base.component.html +++ b/src/frontend/app/features/applications/application/application-tabs-base/application-tabs-base.component.html @@ -1,28 +1,17 @@

{{ (applicationService.application$ | async)?.app.entity.name }}

- - - - - - - launch - - - - + -
diff --git a/src/frontend/app/features/applications/application/application-tabs-base/application-tabs-base.component.ts b/src/frontend/app/features/applications/application/application-tabs-base/application-tabs-base.component.ts index bb9fb88c16..759d5b42c8 100644 --- a/src/frontend/app/features/applications/application/application-tabs-base/application-tabs-base.component.ts +++ b/src/frontend/app/features/applications/application/application-tabs-base/application-tabs-base.component.ts @@ -12,6 +12,7 @@ import { DeleteApplication } from '../../../../store/actions/application.actions import { RouterNav } from '../../../../store/actions/router.actions'; import { AppState } from '../../../../store/app-state'; import { ApplicationService } from '../../application.service'; +import { APIResource } from '../../../../store/types/api.types'; // Confirmation dialogs const appStopConfirmation = new ConfirmationDialog( @@ -42,7 +43,7 @@ export class ApplicationTabsBaseComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private router: Router, private applicationService: ApplicationService, - private entityService: EntityService, + private entityService: EntityService, private store: Store, private confirmDialog: ConfirmationDialogService ) { } @@ -113,9 +114,11 @@ export class ApplicationTabsBaseComponent implements OnInit, OnDestroy { // Auto refresh this.entityServiceAppRefresh$ = this.entityService .poll(10000, this.autoRefreshString) - .do(() => { + .do(({ resource }) => { this.store.dispatch(new GetAppSummaryAction(appGuid, cfGuid)); - this.store.dispatch(new GetAppStatsAction(appGuid, cfGuid)); + if (resource && resource.entity && resource.entity.state === 'STARTED') { + this.store.dispatch(new GetAppStatsAction(appGuid, cfGuid)); + } }) .subscribe(); diff --git a/src/frontend/app/features/applications/application/application-tabs-base/tabs/variables-tab/variables-tab.component.html b/src/frontend/app/features/applications/application/application-tabs-base/tabs/variables-tab/variables-tab.component.html index 0c15bc32b3..f1ac432f0a 100644 --- a/src/frontend/app/features/applications/application/application-tabs-base/tabs/variables-tab/variables-tab.component.html +++ b/src/frontend/app/features/applications/application/application-tabs-base/tabs/variables-tab/variables-tab.component.html @@ -1,7 +1,7 @@
-
+
diff --git a/src/frontend/app/features/applications/application/application-tabs-base/tabs/variables-tab/variables-tab.component.scss b/src/frontend/app/features/applications/application/application-tabs-base/tabs/variables-tab/variables-tab.component.scss index a6278dcca7..76ca0d369b 100644 --- a/src/frontend/app/features/applications/application/application-tabs-base/tabs/variables-tab/variables-tab.component.scss +++ b/src/frontend/app/features/applications/application/application-tabs-base/tabs/variables-tab/variables-tab.component.scss @@ -11,4 +11,13 @@ } } } + &__env-table--add-form { + form { + display: flex; + flex-direction: row; + } + mat-form-field { + padding-right: 10px; + } + } } diff --git a/src/frontend/app/features/applications/create-application/create-application-step1/create-application-step1.component.html b/src/frontend/app/features/applications/create-application/create-application-step1/create-application-step1.component.html index d2afd5df08..a7ae7fd1d7 100644 --- a/src/frontend/app/features/applications/create-application/create-application-step1/create-application-step1.component.html +++ b/src/frontend/app/features/applications/create-application/create-application-step1/create-application-step1.component.html @@ -1,22 +1,28 @@ Select a Cloud Foundry instance, organization and space for the app. - - - - {{ cf.name }} - - - - - - - {{ org.name }} - - - - - - - {{ space.name }} - - + + + + + {{ cf.name }} + + + + + + + + + {{ org.name }} + + + + + + + + + {{ space.name }} + + + diff --git a/src/frontend/app/features/applications/create-application/create-application-step2/create-application-step2.component.html b/src/frontend/app/features/applications/create-application/create-application-step2/create-application-step2.component.html index 989afef1a1..8a85fed3e8 100644 --- a/src/frontend/app/features/applications/create-application/create-application-step2/create-application-step2.component.html +++ b/src/frontend/app/features/applications/create-application/create-application-step2/create-application-step2.component.html @@ -1,5 +1,5 @@ Please select a unique name for your application -
+
diff --git a/src/frontend/app/features/applications/create-application/create-application-step3/create-application-step3.component.html b/src/frontend/app/features/applications/create-application/create-application-step3/create-application-step3.component.html index 5478dbe38d..2d256dc25a 100644 --- a/src/frontend/app/features/applications/create-application/create-application-step3/create-application-step3.component.html +++ b/src/frontend/app/features/applications/create-application/create-application-step3/create-application-step3.component.html @@ -1,10 +1,12 @@ Create a route - - - - {{ domain.entity.name }} - - + + + + + {{ domain.entity.name }} + + + diff --git a/src/frontend/app/features/applications/create-application/create-application-step3/create-application-step3.component.ts b/src/frontend/app/features/applications/create-application/create-application-step3/create-application-step3.component.ts index 8464c650c1..f73bd5d9ed 100644 --- a/src/frontend/app/features/applications/create-application/create-application-step3/create-application-step3.component.ts +++ b/src/frontend/app/features/applications/create-application/create-application-step3/create-application-step3.component.ts @@ -18,8 +18,8 @@ import { AppState } from '../../../../store/app-state'; import { selectNewAppState } from '../../../../store/effects/create-app-effects'; import { CreateNewApplicationState } from '../../../../store/types/create-application.types'; import { RouterNav } from '../../../../store/actions/router.actions'; -import { OrganisationSchema } from '../../../../store/actions/organisation.action'; import { RequestInfoState } from '../../../../store/reducers/api-request-reducer/types'; +import { organisationSchemaKey } from '../../../../store/actions/action-types'; @Component({ selector: 'app-create-application-step3', @@ -124,7 +124,7 @@ export class CreateApplicationStep3Component implements OnInit { }) .filter(state => state.cloudFoundryDetails && state.cloudFoundryDetails.org) .mergeMap(state => { - return this.store.select(selectEntity(OrganisationSchema.key, state.cloudFoundryDetails.org)) + return this.store.select(selectEntity(organisationSchemaKey, state.cloudFoundryDetails.org)) .first() .map(org => org.entity.domains); }); diff --git a/src/frontend/app/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.html b/src/frontend/app/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.html index 6196c12c38..e84e320ac5 100644 --- a/src/frontend/app/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.html +++ b/src/frontend/app/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.html @@ -1,5 +1,5 @@ Please specify the source - +
diff --git a/src/frontend/app/features/applications/deploy-application/deploy-application-step3/deploy-application-step3.component.ts b/src/frontend/app/features/applications/deploy-application/deploy-application-step3/deploy-application-step3.component.ts index e8653b710d..c0aa14ad44 100644 --- a/src/frontend/app/features/applications/deploy-application/deploy-application-step3/deploy-application-step3.component.ts +++ b/src/frontend/app/features/applications/deploy-application/deploy-application-step3/deploy-application-step3.component.ts @@ -4,9 +4,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '../../../../store/app-state'; import { tap, filter, map, mergeMap, combineLatest, switchMap, share, catchError } from 'rxjs/operators'; import { getEntityById, selectEntity, selectEntities } from '../../../../store/selectors/api.selectors'; -import { OrganizationSchema } from '../../../../store/actions/organization.actions'; import { DeleteDeployAppSection } from '../../../../store/actions/deploy-applications.actions'; -import { SpaceSchema } from '../../../../store/actions/space.actions'; import websocketConnect from 'rxjs-websockets'; import { QueueingSubject } from 'queueing-subject/lib'; import { Subscription } from 'rxjs/Subscription'; @@ -20,6 +18,7 @@ import { RouterNav } from '../../../../store/actions/router.actions'; import { GetAllApplications } from '../../../../store/actions/application.actions'; import { environment } from '../../../../../environments/environment'; import { CfOrgSpaceDataService } from '../../../../shared/data-services/cf-org-space-service.service'; +import { organisationSchemaKey, spaceSchemaKey } from '../../../../store/actions/action-types'; @Component({ selector: 'app-deploy-application-step3', @@ -58,9 +57,9 @@ export class DeployApplicationStep3Component implements OnInit, OnDestroy { && !!appDetail.applicationSource && !!appDetail.applicationSource.projectName), mergeMap(p => { - const orgSubscription = this.store.select(selectEntity(OrganizationSchema.key, p.cloudFoundryDetails.org)); - const spaceSubscription = this.store.select(selectEntity(SpaceSchema.key, p.cloudFoundryDetails.space)); - return Observable.of(p).combineLatest(orgSubscription, spaceSubscription ); + const orgSubscription = this.store.select(selectEntity(organisationSchemaKey, p.cloudFoundryDetails.org)); + const spaceSubscription = this.store.select(selectEntity(spaceSchemaKey, p.cloudFoundryDetails.space)); + return Observable.of(p).combineLatest(orgSubscription, spaceSubscription); }), tap(p => { const host = window.location.host; @@ -70,10 +69,10 @@ export class DeployApplicationStep3Component implements OnInit, OnDestroy { `?org=${p[1].entity.name}&space=${p[2].entity.name}` ); - const inputStream = new QueueingSubject(); + const inputStream = new QueueingSubject(); this.messages = websocketConnect(streamUrl, inputStream) - .messages.pipe( - catchError(e => { + .messages.pipe( + catchError(e => { return []; }), share(), @@ -88,21 +87,21 @@ export class DeployApplicationStep3Component implements OnInit, OnDestroy { this.updateTitle(log); } }), - filter((log ) => log.type === SocketEventTypes.DATA), + filter((log) => log.type === SocketEventTypes.DATA), map((log) => { const timesString = moment(log.timestamp * 1000).format('DD/MM/YYYY hh:mm:ss A'); return ( `${timesString}: ${log.message}` ); }) - ); + ); inputStream.next(this.sendProjectInfo(p[0].applicationSource)); }) ).subscribe(); } - sendProjectInfo = (appSource: DeployApplicationSource) => { + sendProjectInfo = (appSource: DeployApplicationSource) => { if (appSource.type.id === 'git') { if (appSource.type.subType === 'github') { return this.sendGitHubSourceMetadata(appSource); @@ -114,7 +113,7 @@ export class DeployApplicationStep3Component implements OnInit, OnDestroy { return ''; } - sendGitHubSourceMetadata = (appSource: DeployApplicationSource) => { + sendGitHubSourceMetadata = (appSource: DeployApplicationSource) => { const github = { project: appSource.projectName, branch: appSource.branch.name, @@ -129,7 +128,7 @@ export class DeployApplicationStep3Component implements OnInit, OnDestroy { return JSON.stringify(msg); } - sendGitUrlSourceMetadata = (appSource: DeployApplicationSource) => { + sendGitUrlSourceMetadata = (appSource: DeployApplicationSource) => { const giturl = { url: appSource.projectName, branch: appSource.branch.name, @@ -155,53 +154,53 @@ export class DeployApplicationStep3Component implements OnInit, OnDestroy { this.appData.org = this.cfOrgSpaceService.org.select.getValue(); this.appData.space = this.cfOrgSpaceService.space.select.getValue(); break; - case SocketEventTypes.EVENT_PUSH_STARTED : - this.streamTitle = 'Deploying...'; - this.store.dispatch(new GetAllApplications('applicationWall')); - break; - case SocketEventTypes.EVENT_PUSH_COMPLETED : - this.streamTitle = 'Deployed'; - this.apps$ = this.store.select(selectEntities('application')).pipe( - tap(apps => { - Object.values(apps).forEach(app => { - if ( - app.entity.space_guid === this.appData.space && - app.entity.cfGuid === this.appData.cloudFoundry && - app.entity.name === this.appData.Name - ) { - this.appGuid = app.entity.guid; - this.validate = Observable.of(true); - } - }); - }) - ).subscribe(); - break; - case SocketEventTypes.CLOSE_SUCCESS : - this.close(log, null, null, true); - break; + case SocketEventTypes.EVENT_PUSH_STARTED: + this.streamTitle = 'Deploying...'; + this.store.dispatch(new GetAllApplications('applicationWall')); + break; + case SocketEventTypes.EVENT_PUSH_COMPLETED: + this.streamTitle = 'Deployed'; + this.apps$ = this.store.select(selectEntities('application')).pipe( + tap(apps => { + Object.values(apps).forEach(app => { + if ( + app.entity.space_guid === this.appData.space && + app.entity.cfGuid === this.appData.cloudFoundry && + app.entity.name === this.appData.Name + ) { + this.appGuid = app.entity.guid; + this.validate = Observable.of(true); + } + }); + }) + ).subscribe(); + break; + case SocketEventTypes.CLOSE_SUCCESS: + this.close(log, null, null, true); + break; case SocketEventTypes.CLOSE_INVALID_MANIFEST: this.close(log, 'Deploy Failed - Invalid manifest!', - 'Failed to deploy app! Please make sure that a valid manifest.yaml was provided!', true); + 'Failed to deploy app! Please make sure that a valid manifest.yaml was provided!', true); break; case SocketEventTypes.CLOSE_NO_MANIFEST: - this.close(log, 'Deploy Failed - No manifest present!', - 'Failed to deploy app! Please make sure that a valid manifest.yaml is present!', true); + this.close(log, 'Deploy Failed - No manifest present!', + 'Failed to deploy app! Please make sure that a valid manifest.yaml is present!', true); break; case SocketEventTypes.CLOSE_FAILED_CLONE: - this.close(log, 'Deploy Failed - Failed to clone repository!', - 'Failed to deploy app! Please make sure the repository is public!', true); + this.close(log, 'Deploy Failed - Failed to clone repository!', + 'Failed to deploy app! Please make sure the repository is public!', true); break; case SocketEventTypes.CLOSE_FAILED_NO_BRANCH: - this.close(log, 'Deploy Failed - Failed to located branch!', - 'Failed to deploy app! Please make sure that branch exists!', true); + this.close(log, 'Deploy Failed - Failed to located branch!', + 'Failed to deploy app! Please make sure that branch exists!', true); break; case SocketEventTypes.CLOSE_FAILURE: case SocketEventTypes.CLOSE_PUSH_ERROR: case SocketEventTypes.CLOSE_NO_SESSION: case SocketEventTypes.CLOSE_NO_CNSI: case SocketEventTypes.CLOSE_NO_CNSI_USERTOKEN: - this.close(log, 'Deploy Failed!', - 'Failed to deploy app!', true); + this.close(log, 'Deploy Failed!', + 'Failed to deploy app!', true); break; case SocketEventTypes.SOURCE_REQUIRED: case SocketEventTypes.EVENT_CLONED: @@ -209,8 +208,8 @@ export class DeployApplicationStep3Component implements OnInit, OnDestroy { case SocketEventTypes.MANIFEST: break; default: - // noop - } + // noop + } } close(log, title, error, deleteAppSection) { diff --git a/src/frontend/app/features/applications/edit-application/edit-application.component.html b/src/frontend/app/features/applications/edit-application/edit-application.component.html index adf7442457..965a1589e8 100644 --- a/src/frontend/app/features/applications/edit-application/edit-application.component.html +++ b/src/frontend/app/features/applications/edit-application/edit-application.component.html @@ -9,14 +9,14 @@

Edit Application: {{ (applicationService.application$ | async)?.app.entity.n
- + Application name is required Application name already taken
-
+
@@ -28,7 +28,7 @@

Edit Application: {{ (applicationService.application$ | async)?.app.entity.n Enable SSH to Application Instances - Production Application + Production Application

There was an error while updating the application.

diff --git a/src/frontend/app/features/applications/routes/add-routes/add-routes.component.html b/src/frontend/app/features/applications/routes/add-routes/add-routes.component.html index 60d5af981c..9ad995b467 100644 --- a/src/frontend/app/features/applications/routes/add-routes/add-routes.component.html +++ b/src/frontend/app/features/applications/routes/add-routes/add-routes.component.html @@ -19,7 +19,7 @@ Create TCP Route
-
+
@@ -41,7 +41,7 @@
-
+
diff --git a/src/frontend/app/features/applications/routes/add-routes/add-routes.component.scss b/src/frontend/app/features/applications/routes/add-routes/add-routes.component.scss index f1bf78842c..41846fea35 100644 --- a/src/frontend/app/features/applications/routes/add-routes/add-routes.component.scss +++ b/src/frontend/app/features/applications/routes/add-routes/add-routes.component.scss @@ -32,7 +32,7 @@ margin-bottom: 24px; } &__toggle { - margin-bottom: 24px; + margin-bottom: 10px; margin-top: 24px; mat-checkbox { margin-left: 12px; diff --git a/src/frontend/app/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html b/src/frontend/app/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html index 6a7f0bb10e..f7d8d14b03 100644 --- a/src/frontend/app/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html +++ b/src/frontend/app/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.html @@ -2,21 +2,19 @@ Register an existing Cloud Foundry endpoint to allow your development team to create and manage their applications.

- When registering, choose a unique, recognizable name so that your developers can easily know which Cloud Foundry endpoint - they are working with. + When registering, choose a unique, recognizable name so that your developers can easily know which Cloud Foundry endpoint they are working with.

Supply the API Url for your Cloud Foundry endpoint

- + Name is required Name is not unique - + URL is required Invalid API URL URL is not unique diff --git a/src/frontend/app/features/uaa-setup/uaa-wizard/console-uaa-wizard.component.html b/src/frontend/app/features/uaa-setup/uaa-wizard/console-uaa-wizard.component.html index e4b78b3475..2a21614446 100644 --- a/src/frontend/app/features/uaa-setup/uaa-wizard/console-uaa-wizard.component.html +++ b/src/frontend/app/features/uaa-setup/uaa-wizard/console-uaa-wizard.component.html @@ -4,15 +4,14 @@

Welcome to SUSE Cloud Foundry Console.

- SUSE Cloud Foundry Console is an Open Source Web-based UI (Console) for managing Cloud Foundry. It allows users and administrators - to both manage applications running in the Cloud Foundry cluster and perform cluster management tasks. + SUSE Cloud Foundry Console is an Open Source Web-based UI (Console) for managing Cloud Foundry. It allows users and administrators to both manage applications running in the Cloud Foundry cluster and perform cluster management tasks.

Before accessing the console for the first time some configuration information is required. Press NEXT to get started.

- + UAA Endpoint @@ -33,7 +32,7 @@ -
+ {{ scope }} diff --git a/src/frontend/app/shared/components/cards/card-app-uptime/card-app-uptime.component.html b/src/frontend/app/shared/components/cards/card-app-uptime/card-app-uptime.component.html index daed8f9887..bdd6549972 100644 --- a/src/frontend/app/shared/components/cards/card-app-uptime/card-app-uptime.component.html +++ b/src/frontend/app/shared/components/cards/card-app-uptime/card-app-uptime.component.html @@ -5,12 +5,12 @@ Uptime -
{{ appData.monitor.max.uptime | uptime }}
+
{{ appData.maxUptime | uptime }}
-
-
- {{ appData.monitor.avg.uptime | uptime }} - {{ appData.monitor.min.uptime | uptime }} +
+
+ {{ appData.averageUptime | uptime }} + {{ appData.minUptime | uptime }}
diff --git a/src/frontend/app/shared/components/cards/card-app-uptime/card-app-uptime.component.ts b/src/frontend/app/shared/components/cards/card-app-uptime/card-app-uptime.component.ts index b00fd924a1..66e6132d7e 100644 --- a/src/frontend/app/shared/components/cards/card-app-uptime/card-app-uptime.component.ts +++ b/src/frontend/app/shared/components/cards/card-app-uptime/card-app-uptime.component.ts @@ -1,7 +1,9 @@ import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { map, startWith } from 'rxjs/operators'; + import { ApplicationMonitorService } from '../../../../features/applications/application-monitor.service'; import { ApplicationService } from '../../../../features/applications/application.service'; -import { Observable } from 'rxjs/Observable'; @Component({ selector: 'app-card-app-uptime', @@ -12,13 +14,27 @@ export class CardAppUptimeComponent implements OnInit { constructor(private appService: ApplicationService, private appMonitor: ApplicationMonitorService) { } - appData$: Observable; + appData$: Observable<{ + maxUptime: number, + minUptime: number, + averageUptime: number, + runningCount: number + }>; ngOnInit() { - this.appData$ = Observable.combineLatest( - this.appMonitor.appMonitor$, - this.appService.application$.map(data => data.app.entity.state === 'STARTED'), - (monitor, isRunning) => ({ monitor: monitor, isRunning: isRunning, status: !isRunning ? 'tentative' : monitor.status.usage }) + this.appData$ = this.appMonitor.appMonitor$.pipe( + map(monitor => ({ + maxUptime: monitor.max.uptime, + minUptime: monitor.min.uptime, + averageUptime: monitor.avg.uptime, + runningCount: monitor.running + })), + startWith({ + maxUptime: 0, + minUptime: 0, + averageUptime: 0, + runningCount: 0 + }) ); } } diff --git a/src/frontend/app/shared/components/list/data-sources-controllers/list-data-source.ts b/src/frontend/app/shared/components/list/data-sources-controllers/list-data-source.ts index db676ba914..975083f3f7 100644 --- a/src/frontend/app/shared/components/list/data-sources-controllers/list-data-source.ts +++ b/src/frontend/app/shared/components/list/data-sources-controllers/list-data-source.ts @@ -6,22 +6,21 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { OperatorFunction } from 'rxjs/interfaces'; import { Observable } from 'rxjs/Observable'; import { combineLatest } from 'rxjs/observable/combineLatest'; -import { distinctUntilChanged, tap, pairwise, filter } from 'rxjs/operators'; -import { map, shareReplay } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, pairwise, publishReplay, refCount, shareReplay, tap } from 'rxjs/operators'; import { Subscription } from 'rxjs/Subscription'; -import { SetResultCount, CreatePagination } from '../../../../store/actions/pagination.actions'; +import { CreatePagination, SetResultCount } from '../../../../store/actions/pagination.actions'; import { AppState } from '../../../../store/app-state'; import { getCurrentPageRequestInfo, getPaginationObservables, } from '../../../../store/reducers/pagination-reducer/pagination-reducer.helper'; +import { selectPaginationState } from '../../../../store/selectors/pagination.selectors'; import { PaginatedAction, PaginationEntityState } from '../../../../store/types/pagination.types'; +import { PaginationMonitor } from '../../../monitors/pagination-monitor'; import { IListDataSourceConfig } from './list-data-source-config'; import { getDefaultRowState, getRowUniqueId, IListDataSource, RowsState } from './list-data-source-types'; import { getDataFunctionList } from './local-filtering-sorting'; -import { PaginationMonitor } from '../../../monitors/pagination-monitor'; -import { selectPaginationState } from '../../../../store/selectors/pagination.selectors'; export class DataFunctionDefinition { type: 'sort' | 'filter'; @@ -115,8 +114,13 @@ export abstract class ListDataSource extends DataSource implements // Add any additional functions via an optional listConfig, such as sorting from the column definition const listColumns = this.config.listConfig ? this.config.listConfig.getColumns() : []; listColumns.forEach(column => { + if (!column.sort) { + return; + } if (DataFunctionDefinition.is(column.sort)) { transformEntities.push(column.sort as DataFunctionDefinition); + } else if (typeof column.sort !== 'boolean') { + transformEntities.push(column.sort as DataFunction); } }); @@ -266,24 +270,33 @@ export abstract class ListDataSource extends DataSource implements }); } }), + filter(([paginationEntity, entities]) => !getCurrentPageRequestInfo(paginationEntity).busy), map(([paginationEntity, entities]) => { + if (entities && !entities.length) { + return []; + } + const entitiesPreFilter = entities.length; if (dataFunctions && dataFunctions.length) { entities = dataFunctions.reduce((value, fn) => { return fn(value, paginationEntity); }, entities); } + const entitiesPostFilter = entities.length; const pages = this.splitClientPages(entities, paginationEntity.clientPagination.pageSize); if ( - paginationEntity.totalResults !== entities.length || - paginationEntity.clientPagination.totalResults !== entities.length + entitiesPreFilter !== entitiesPostFilter && + (paginationEntity.totalResults !== entities.length || + paginationEntity.clientPagination.totalResults !== entities.length) ) { this.store.dispatch(new SetResultCount(this.entityKey, this.paginationKey, entities.length)); } + const pageIndex = paginationEntity.clientPagination.currentPage - 1; return pages[pageIndex]; }), - shareReplay(1), + publishReplay(1), + refCount(), tag('local-list') ); } diff --git a/src/frontend/app/shared/components/list/list-table/table.component.html b/src/frontend/app/shared/components/list/list-table/table.component.html index e73f029d41..f7eea57052 100644 --- a/src/frontend/app/shared/components/list/list-table/table.component.html +++ b/src/frontend/app/shared/components/list/list-table/table.component.html @@ -1,7 +1,5 @@
- - diff --git a/src/frontend/app/shared/components/list/list-table/table.component.scss b/src/frontend/app/shared/components/list/list-table/table.component.scss index 06a34312de..4b4c038849 100644 --- a/src/frontend/app/shared/components/list/list-table/table.component.scss +++ b/src/frontend/app/shared/components/list/list-table/table.component.scss @@ -2,7 +2,6 @@ $row-height: 62px; .app-table { &__card { padding: 0; - padding-top: 24px; } &__inner { &[hidden] * { diff --git a/src/frontend/app/shared/components/list/list-table/table.types.ts b/src/frontend/app/shared/components/list/list-table/table.types.ts index 1e9a99f898..e8a51c8399 100644 --- a/src/frontend/app/shared/components/list/list-table/table.types.ts +++ b/src/frontend/app/shared/components/list/list-table/table.types.ts @@ -1,4 +1,4 @@ -import { DataFunctionDefinition } from '../data-sources-controllers/list-data-source'; +import { DataFunction, DataFunctionDefinition } from '../data-sources-controllers/list-data-source'; import { TableCellStatusDirective } from './table-cell-status.directive'; import { listTableCells, TableCellComponent } from './table-cell/table-cell.component'; import { TableRowComponent } from './table-row/table-row.component'; @@ -12,7 +12,7 @@ export interface ITableColumn { headerCell?: () => string; // Either headerCell OR headerCellComponent should be defined headerCellComponent?: any; class?: string; - sort?: boolean | DataFunctionDefinition; + sort?: boolean | DataFunctionDefinition | DataFunction; cellFlex?: string; } diff --git a/src/frontend/app/shared/components/list/list-types/app-route/cf-app-map-routes-list-config.service.ts b/src/frontend/app/shared/components/list/list-types/app-route/cf-app-map-routes-list-config.service.ts index 2954d71c91..2075fb2fa0 100644 --- a/src/frontend/app/shared/components/list/list-types/app-route/cf-app-map-routes-list-config.service.ts +++ b/src/frontend/app/shared/components/list/list-types/app-route/cf-app-map-routes-list-config.service.ts @@ -1,3 +1,4 @@ +import { isTCPRoute } from '../../../../../features/applications/routes/routes.helper'; import { Injectable } from '@angular/core'; import { MatSnackBar } from '@angular/material'; import { ActivatedRoute } from '@angular/router'; @@ -25,10 +26,10 @@ import { TableCellAppRouteComponent } from './table-cell-app-route/table-cell-ap import { TableCellRadioComponent } from './table-cell-radio/table-cell-radio.component'; import { TableCellRouteComponent } from './table-cell-route/table-cell-route.component'; import { TableCellTCPRouteComponent } from './table-cell-tcproute/table-cell-tcproute.component'; +import { PaginationEntityState } from '../../../../../store/types/pagination.types'; @Injectable() -export class CfAppMapRoutesListConfigService - implements IListConfig { +export class CfAppMapRoutesListConfigService implements IListConfig { routesDataSource: CfAppRoutesDataSource; columns: Array> = [ @@ -43,30 +44,43 @@ export class CfAppMapRoutesListConfigService columnId: 'route', headerCell: () => 'Route', cellComponent: TableCellRouteComponent, - sort: true, + sort: { + type: 'sort', + orderKey: 'route', + field: 'entity.host' + }, cellFlex: '3' }, { columnId: 'tcproute', headerCell: () => 'TCP Route', cellComponent: TableCellTCPRouteComponent, - sort: true, + sort: { + type: 'sort', + orderKey: 'tcproute', + field: 'entity.isTCPRoute' + }, cellFlex: '3' }, { columnId: 'attachedApps', headerCell: () => 'Apps Attached', cellComponent: TableCellAppRouteComponent, - sort: true, + sort: { + type: 'sort', + orderKey: 'attachedApps', + field: 'entity.mappedAppsCount' + }, cellFlex: '3' } ]; - pageSizeOptions = [9, 45, 90]; + pageSizeOptions = [5, 15, 30]; viewType = ListViewTypes.TABLE_ONLY; - text: { - title: 'Available Routes'; + text = { + title: 'Available Routes' }; + isLocal = true; dispatchDeleteAction(route) { return this.store.dispatch( @@ -104,7 +118,8 @@ export class CfAppMapRoutesListConfigService this.appService, new GetSpaceRoutes(spaceGuid, appService.cfGuid), getPaginationKey('route', appService.cfGuid, spaceGuid), - true + true, + this ); } } diff --git a/src/frontend/app/shared/components/list/list-types/app-route/cf-app-routes-data-source.ts b/src/frontend/app/shared/components/list/list-types/app-route/cf-app-routes-data-source.ts index 113909c079..2f775ebcfd 100644 --- a/src/frontend/app/shared/components/list/list-types/app-route/cf-app-routes-data-source.ts +++ b/src/frontend/app/shared/components/list/list-types/app-route/cf-app-routes-data-source.ts @@ -8,6 +8,8 @@ import { APIResource, EntityInfo } from '../../../../../store/types/api.types'; import { PaginatedAction } from '../../../../../store/types/pagination.types'; import { ListDataSource } from '../../data-sources-controllers/list-data-source'; import { IListConfig } from '../../list.component.types'; +import { map } from 'rxjs/operators'; +import { isTCPRoute, getMappedApps } from '../../../../../features/applications/routes/routes.helper'; export const RouteSchema = new schema.Entity('route'); @@ -20,7 +22,8 @@ export class CfAppRoutesDataSource extends ListDataSource { appService: ApplicationService, action: PaginatedAction, paginationKey: string, - mapRoute = false + mapRoute = false, + listConfig: IListConfig ) { super({ store, @@ -28,7 +31,26 @@ export class CfAppRoutesDataSource extends ListDataSource { schema: RouteSchema, getRowUniqueId: (object: EntityInfo) => object.entity ? object.entity.guid : null, - paginationKey + paginationKey, + isLocal: true, + listConfig, + transformEntity: map((routes) => { + routes = routes.map(route => { + let newRoute = route; + if (!route.entity.isTCPRoute || !route.entity.mappedAppsCount) { + newRoute = { + ...route, + entity: { + ...route.entity, + isTCPRoute: isTCPRoute(route), + mappedAppsCount: getMappedApps(route).length + } + }; + } + return newRoute; + }); + return routes; + }) }); this.cfGuid = appService.cfGuid; diff --git a/src/frontend/app/shared/components/list/list-types/app-route/cf-app-routes-list-config.service.ts b/src/frontend/app/shared/components/list/list-types/app-route/cf-app-routes-list-config.service.ts index 521291e6f8..82b5742ff0 100644 --- a/src/frontend/app/shared/components/list/list-types/app-route/cf-app-routes-list-config.service.ts +++ b/src/frontend/app/shared/components/list/list-types/app-route/cf-app-routes-list-config.service.ts @@ -87,22 +87,22 @@ export class CfAppRoutesListConfigService implements IListConfig { action: () => { this.appService.application$ .pipe( - take(1), - tap(app => { - this.store.dispatch( - new RouterNav({ - path: [ - 'applications', - this.appService.cfGuid, - this.appService.appGuid, - 'add-route' - ], - query: { - spaceGuid: app.app.entity.space_guid - } - }) - ); - }) + take(1), + tap(app => { + this.store.dispatch( + new RouterNav({ + path: [ + 'applications', + this.appService.cfGuid, + this.appService.appGuid, + 'add-route' + ], + query: { + spaceGuid: app.app.entity.space_guid + } + }) + ); + }) ) .subscribe(); }, @@ -118,21 +118,32 @@ export class CfAppRoutesListConfigService implements IListConfig { columnId: 'route', headerCell: () => 'Route', cellComponent: TableCellRouteComponent, - cellFlex: '4' + cellFlex: '4', + sort: { + type: 'sort', + orderKey: 'route', + field: 'entity.host' + } }, { columnId: 'tcproute', headerCell: () => 'TCP Route', cellComponent: TableCellTCPRouteComponent, - cellFlex: '4' + cellFlex: '4', + sort: { + type: 'sort', + orderKey: 'tcproute', + field: 'entity.isTCPRoute' + }, } ]; - pageSizeOptions = [9, 45, 90]; + pageSizeOptions = [5, 15, 30]; viewType = ListViewTypes.TABLE_ONLY; text = { title: 'Routes' }; + isLocal = true; dispatchDeleteAction(route) { return this.store.dispatch( @@ -169,7 +180,9 @@ export class CfAppRoutesListConfigService implements IListConfig { this.store, this.appService, new GetAppRoutes(appService.appGuid, appService.cfGuid), - getPaginationKey('route', appService.cfGuid, appService.appGuid) + getPaginationKey('route', appService.cfGuid, appService.appGuid), + false, + this ); } diff --git a/src/frontend/app/shared/components/list/list-types/app-route/table-cell-app-route/table-cell-app-route.component.ts b/src/frontend/app/shared/components/list/list-types/app-route/table-cell-app-route/table-cell-app-route.component.ts index 3dee651648..256da4b317 100644 --- a/src/frontend/app/shared/components/list/list-types/app-route/table-cell-app-route/table-cell-app-route.component.ts +++ b/src/frontend/app/shared/components/list/list-types/app-route/table-cell-app-route/table-cell-app-route.component.ts @@ -20,7 +20,7 @@ export class TableCellAppRouteComponent extends TableCellCustom ngOnInit(): void { const apps = this.row.entity.apps; - this.mappedAppsCount = getMappedApps(this.row).length; + this.mappedAppsCount = this.row.entity.mappedAppsCount; const foundApp = apps && apps.find(a => a.metadata.guid === this.appService.appGuid); if (foundApp && foundApp.length !== 0) { diff --git a/src/frontend/app/shared/components/list/list-types/app-route/table-cell-tcproute/table-cell-tcproute.component.html b/src/frontend/app/shared/components/list/list-types/app-route/table-cell-tcproute/table-cell-tcproute.component.html index eb48b9781c..99a0cebf5a 100644 --- a/src/frontend/app/shared/components/list/list-types/app-route/table-cell-tcproute/table-cell-tcproute.component.html +++ b/src/frontend/app/shared/components/list/list-types/app-route/table-cell-tcproute/table-cell-tcproute.component.html @@ -1,8 +1,8 @@
-
+
Yes
-
+
No
diff --git a/src/frontend/app/shared/components/list/list-types/app-route/table-cell-tcproute/table-cell-tcproute.component.ts b/src/frontend/app/shared/components/list/list-types/app-route/table-cell-tcproute/table-cell-tcproute.component.ts index 4c68f3592a..217880e089 100644 --- a/src/frontend/app/shared/components/list/list-types/app-route/table-cell-tcproute/table-cell-tcproute.component.ts +++ b/src/frontend/app/shared/components/list/list-types/app-route/table-cell-tcproute/table-cell-tcproute.component.ts @@ -7,16 +7,10 @@ import { isTCPRoute } from '../../../../../../features/applications/routes/route templateUrl: './table-cell-tcproute.component.html', styleUrls: ['./table-cell-tcproute.component.scss'] }) -export class TableCellTCPRouteComponent extends TableCellCustom implements OnInit { +export class TableCellTCPRouteComponent extends TableCellCustom { @Input('row') row; - isRouteTCP: boolean; constructor() { super(); } - - ngOnInit() { - this.isRouteTCP = isTCPRoute(this.row); - } - } diff --git a/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index d63e40ccaf..433e92e96b 100644 --- a/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/app/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -6,12 +6,11 @@ import { ConnectEndpointDialogComponent, } from '../../../../../features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component'; import { DisconnectEndpoint, UnregisterEndpoint } from '../../../../../store/actions/endpoint.actions'; -import { ResetPagination } from '../../../../../store/actions/pagination.actions'; import { ShowSnackBar } from '../../../../../store/actions/snackBar.actions'; import { GetSystemInfo } from '../../../../../store/actions/system.actions'; import { AppState } from '../../../../../store/app-state'; import { EndpointsEffect } from '../../../../../store/effects/endpoint.effects'; -import { selectUpdateInfo } from '../../../../../store/selectors/api.selectors'; +import { selectDeletionInfo, selectUpdateInfo } from '../../../../../store/selectors/api.selectors'; import { EndpointModel, endpointStoreNames } from '../../../../../store/types/endpoint.types'; import { ITableColumn } from '../../list-table/table.types'; import { IListAction, IListConfig, IMultiListAction, ListViewTypes } from '../../list.component.types'; @@ -76,9 +75,8 @@ export class EndpointsListConfigService implements IListConfig { private listActionDelete: IListAction = { action: (item) => { this.store.dispatch(new UnregisterEndpoint(item.guid)); - this.handleAction(item, EndpointsEffect.unregisteringKey, ([oldVal, newVal]) => { + this.handleDeleteAction(item, ([oldVal, newVal]) => { this.store.dispatch(new ShowSnackBar(`Unregistered ${item.name}`)); - this.store.dispatch(new ResetPagination(this.dataSource.entityKey, this.dataSource.paginationKey)); }); }, icon: 'delete', @@ -102,7 +100,7 @@ export class EndpointsListConfigService implements IListConfig { private listActionDisconnect: IListAction = { action: (item) => { this.store.dispatch(new DisconnectEndpoint(item.guid)); - this.handleAction(item, EndpointsEffect.disconnectingKey, ([oldVal, newVal]) => { + this.handleUpdateAction(item, EndpointsEffect.disconnectingKey, ([oldVal, newVal]) => { this.store.dispatch(new ShowSnackBar(`Disconnected ${item.name}`)); this.store.dispatch(new GetSystemInfo()); }); @@ -153,12 +151,23 @@ export class EndpointsListConfigService implements IListConfig { enableTextFilter = true; tableFixedRowHeight = true; - private handleAction(item, effectKey, handleChange) { - const disSub = this.store.select(selectUpdateInfo( + private handleUpdateAction(item, effectKey, handleChange) { + this.handleAction(selectUpdateInfo( endpointStoreNames.type, item.guid, effectKey, - )) + ), handleChange); + } + + private handleDeleteAction(item, handleChange) { + this.handleAction(selectDeletionInfo( + endpointStoreNames.type, + item.guid, + ), handleChange); + } + + private handleAction(storeSelect, handleChange) { + const disSub = this.store.select(storeSelect) .pairwise() .subscribe(([oldVal, newVal]) => { // https://github.com/SUSE/stratos/issues/29 Generic way to handle errors ('Failed to disconnect X') diff --git a/src/frontend/app/shared/components/list/list.component.html b/src/frontend/app/shared/components/list/list.component.html index a9a37cb764..457152e11e 100644 --- a/src/frontend/app/shared/components/list/list.component.html +++ b/src/frontend/app/shared/components/list/list.component.html @@ -1,109 +1,103 @@ -
-
- - -
-
-
{{ config.text?.title }}
-
{{dataSource.selectedRows.size}} Selected
-
-
-
- - - -
-
- - - -
-
-
- - - -
-
- -
-
- - -
-
+
+
+ + +
+
{{ config.text?.title }}
+
{{dataSource.selectedRows.size}} Selected
+ +
+ + + All + + {{selectItem.label}} + + + {{multiFilterConfig.label}} +
-
-
-
- - - All - - {{selectItem.label}} - - - -
-
-
-
- - - - - - {{column.headerCell()}} - - - -
-
- - - -
-
+
+
+ +
+ + + +
+ +
+ + + + {{column.headerCell()}} + + + + + +
+ +
+ + + +
+ +
+ + + +
+ +
+
+ + + +
+ +
+ +
+ +
+ +
- - -
-
-
-
+
-
- - + +
- + - +
There are no entries.
diff --git a/src/frontend/app/shared/components/list/list.component.scss b/src/frontend/app/shared/components/list/list.component.scss index cc4c5428ca..dd04f061b0 100644 --- a/src/frontend/app/shared/components/list/list.component.scss +++ b/src/frontend/app/shared/components/list/list.component.scss @@ -1,9 +1,26 @@ .list-component { - *[hidden], - .sort[hidden], - .filter[hidden], - .add-container[hidden] { - display: none; + *, + .sort, + .filter, + .add-container { + &[hidden] { + display: none; + } + } + &__cards { + .list-component__header-card { + margin-bottom: 20px; + } + } + &__table { + .list-component__header-card { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + .list-component__paginator { + border-top-left-radius: 0; + border-top-right-radius: 0; + } } &__blocker { bottom: 0; @@ -16,69 +33,61 @@ &__body-inner { position: relative; } - &__header { - &__left[hidden], - &__right[hidden], - &__top[hidden], - &__bottom[hidden] { - display: none; - } - } $title-left-padding: 25px; $title-content-padding: 10px; &__header { - mat-card-header { - padding: 15px; + position: relative; + mat-progress-bar { + position: absolute; + top: -5px; } - &__left, - &__right, - &__top, - &__bottom { + mat-card { display: flex; - flex-direction: row; - align-items: center; - height: 55px; + } + &-card { + flex-wrap: wrap; + justify-content: space-between; } &__left, &__right { - > div:not(:last-of-type) { - padding-right: $title-content-padding; + align-items: center; + display: flex; + flex-direction: row; + > div { + &:not(:last-of-type) { + padding-right: $title-content-padding; + } + &:not([hidden]) { + align-items: center; + display: flex; + } } } - &__left .multi-filters mat-form-field { - padding-right: $title-content-padding; - } - &__right { - .add-container { - display: flex; - align-items: center; + &__left { + flex: 1; + &--text { + padding: 10px; } - .mat-paginator { - padding: 0; - } - } - &__bottom { - justify-content: flex-end; - align-items: flex-end; - } - mat-card { - padding: 0 10px; - mat-card-header { - display: flex; - flex-direction: column; - div:not([hidden]).spacer { - flex-grow: 99; + &--multi-filters { + mat-form-field { + padding-right: $title-content-padding; } } } + > mat-card { + min-height: 74px; + padding: 0 24px; + width: 100%; + } } &__body { - padding-top: 15px; .no-rows { padding: 20px $title-left-padding; } - mat-card[hidden] { - display: none; + mat-card { + &[hidden] { + display: none; + } } } &__paginator { diff --git a/src/frontend/app/shared/components/list/list.component.theme.scss b/src/frontend/app/shared/components/list/list.component.theme.scss index 9e7681e42e..6c51012b83 100644 --- a/src/frontend/app/shared/components/list/list.component.theme.scss +++ b/src/frontend/app/shared/components/list/list.component.theme.scss @@ -1,9 +1,20 @@ @import '~@angular/material/theming'; -@mixin variables-tab-theme($theme, $app-theme) { +@import '../../../../sass/mixins'; +@mixin list-theme($theme, $app-theme) { $primary: map-get($theme, primary); $foreground-colors: map-get($theme, foreground); $background-colors: map-get($theme, background); - .list-component__header.selected .mat-card { - background-color: mat-color($primary, 50); + .list-component { + &__header { + &--selected { + background-color: mat-color($primary, 50); + } + &__right, + &__left--multi-filters { + @include breakpoint(mobileonly) { + flex-wrap: wrap; + } + } + } } } diff --git a/src/frontend/app/shared/components/list/list.component.ts b/src/frontend/app/shared/components/list/list.component.ts index 1d5205e768..91cc6a747b 100644 --- a/src/frontend/app/shared/components/list/list.component.ts +++ b/src/frontend/app/shared/components/list/list.component.ts @@ -29,6 +29,8 @@ import { IListConfig, ListViewTypes, } from './list.component.types'; +import { combineLatest } from 'rxjs/observable/combineLatest'; +import { map } from 'rxjs/operators'; @Component({ @@ -64,6 +66,9 @@ export class ListComponent implements OnInit, OnDestroy, AfterViewInit { paginationController: IListPaginationController; multiFilterWidgetObservables = new Array(); + isAddingOrSelecting$: Observable; + hasRows$: Observable; + public safeAddForm() { // Something strange is afoot. When using addform in [disabled] it thinks this is null, even when initialised // When applying the question mark (addForm?) it's value is ignored by [disabled] @@ -84,11 +89,20 @@ export class ListComponent implements OnInit, OnDestroy, AfterViewInit { this.dataSource = this.config.getDataSource(); this.multiFilterConfigs = this.config.getMultiFiltersConfigs(); - // Set up an obervable containing the current view (card/table) + // Create convenience observables that make the html clearer + this.isAddingOrSelecting$ = combineLatest( + this.dataSource.isAdding$, + this.dataSource.isSelecting$ + ).pipe( + map(([isAdding, isSelecting]) => isAdding || isSelecting) + ); + this.hasRows$ = this.dataSource.pagination$.map(pag => pag.totalResults > 0); + + // Set up an observable containing the current view (card/table) const { view, } = getListStateObservables(this.store, this.dataSource.paginationKey); this.view$ = view; - // If this is the first time the user has used this lis then set the view to the default + // If this is the first time the user has used this list then set the view to the default this.view$.first().subscribe(listView => { if (!listView) { this.updateListView(this.getDefaultListView(this.config)); @@ -98,6 +112,15 @@ export class ListComponent implements OnInit, OnDestroy, AfterViewInit { this.paginationController = new ListPaginationController(this.store, this.dataSource); this.paginator.pageSizeOptions = this.config.pageSizeOptions; + + // Ensure we set a pageSize that's relevant to the configured set of page sizes. The default is 9 and in some cases is not a valid + // pageSize + this.paginationController.pagination$.first().subscribe(pagination => { + if (this.paginator.pageSizeOptions.findIndex(pageSize => pageSize === pagination.pageSize) < 0) { + this.paginationController.pageSize(this.paginator.pageSizeOptions[0]); + } + }); + const paginationStoreToWidget = this.paginationController.pagination$.do((pagination: ListPagination) => { this.paginator.length = pagination.totalResults; this.paginator.pageIndex = pagination.pageIndex - 1; diff --git a/src/frontend/app/shared/components/stepper/steppers/steppers.component.scss b/src/frontend/app/shared/components/stepper/steppers/steppers.component.scss index 3c7e870957..3372b5febc 100644 --- a/src/frontend/app/shared/components/stepper/steppers/steppers.component.scss +++ b/src/frontend/app/shared/components/stepper/steppers/steppers.component.scss @@ -71,13 +71,14 @@ flex: 1; text-align: right; } - form { + .stepper-form { + // Use a specific style instead of form element selected. This prevents these generic styles bleading into child components of a stepper display: flex; flex-direction: column; + margin-top: 10px; max-width: 60%; - mat-select, mat-form-field { - margin-top: 20px; + padding-top: 10px; width: 100%; } } diff --git a/src/frontend/app/shared/data-services/cf-org-space-service.service.ts b/src/frontend/app/shared/data-services/cf-org-space-service.service.ts index b1c80d6657..c4eceaecca 100644 --- a/src/frontend/app/shared/data-services/cf-org-space-service.service.ts +++ b/src/frontend/app/shared/data-services/cf-org-space-service.service.ts @@ -3,12 +3,13 @@ import { Store } from '@ngrx/store'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { Observable } from 'rxjs/Observable'; -import { GetAllOrganizations, OrganizationSchema } from '../../store/actions/organization.actions'; import { AppState } from '../../store/app-state'; import { getPaginationObservables, getCurrentPageRequestInfo } from '../../store/reducers/pagination-reducer/pagination-reducer.helper'; import { endpointsRegisteredEntitiesSelector } from '../../store/selectors/endpoint.selectors'; import { EndpointModel } from '../../store/types/endpoint.types'; import { PaginationMonitorFactory } from '../monitors/pagination-monitor.factory'; +import { GetAllOrganisations } from '../../store/actions/organisation.actions'; +import { OrganisationWithSpaceSchema } from '../../store/actions/action-types'; export interface CfOrgSpaceItem { list$: Observable; @@ -25,7 +26,7 @@ export class CfOrgSpaceDataService { public org: CfOrgSpaceItem; public space: CfOrgSpaceItem; - public paginationAction = new GetAllOrganizations(CfOrgSpaceDataService.CfOrgSpaceServicePaginationKey); + public paginationAction = new GetAllOrganisations(CfOrgSpaceDataService.CfOrgSpaceServicePaginationKey); // TODO: We should optimise this to only fetch the orgs for the current endpoint // (if we inline depth the get orgs request it could be hefty... or we could use a different action to only fetch required data.. @@ -35,7 +36,7 @@ export class CfOrgSpaceDataService { action: this.paginationAction, paginationMonitor: this.paginationMonitorFactory.create( this.paginationAction.paginationKey, - OrganizationSchema + OrganisationWithSpaceSchema ) }); diff --git a/src/frontend/app/shared/monitors/pagination-monitor.ts b/src/frontend/app/shared/monitors/pagination-monitor.ts index 6605bcc360..b43c92913a 100644 --- a/src/frontend/app/shared/monitors/pagination-monitor.ts +++ b/src/frontend/app/shared/monitors/pagination-monitor.ts @@ -117,7 +117,7 @@ export class PaginationMonitor { return page.length ? denormalize(page, [schema], allEntities).filter(ent => !!ent) : []; }), shareReplay(1) - ); + ); } private createErrorObservable(pagination$: Observable) { diff --git a/src/frontend/app/shared/pipes/uptime.pipe.ts b/src/frontend/app/shared/pipes/uptime.pipe.ts index 55ee425443..aac893b611 100644 --- a/src/frontend/app/shared/pipes/uptime.pipe.ts +++ b/src/frontend/app/shared/pipes/uptime.pipe.ts @@ -7,9 +7,12 @@ import { UtilsService } from '../../core/utils.service'; }) export class UptimePipe implements PipeTransform { - constructor(private utils: UtilsService) {} + constructor(private utils: UtilsService) { } transform(uptime): string { + if (uptime === 'offline') { + return 'Offline'; + } return this.utils.formatUptime(uptime); } } diff --git a/src/frontend/app/store/actions/action-types.ts b/src/frontend/app/store/actions/action-types.ts new file mode 100644 index 0000000000..8c36f51a38 --- /dev/null +++ b/src/frontend/app/store/actions/action-types.ts @@ -0,0 +1,28 @@ +import { schema } from 'normalizr'; +import { getAPIResourceGuid } from '../selectors/api.selectors'; + +export const organisationSchemaKey = 'organization'; +export const OrganisationSchema = new schema.Entity(organisationSchemaKey, {}, { + idAttribute: getAPIResourceGuid +}); + +export const spaceSchemaKey = 'space'; +export const SpaceSchema = new schema.Entity(spaceSchemaKey, {}, { + idAttribute: getAPIResourceGuid +}); + +export const OrganisationWithSpaceSchema = new schema.Entity(organisationSchemaKey, { + entity: { + spaces: [SpaceSchema] + } +}, { + idAttribute: getAPIResourceGuid + }); + +export const SpaceWithOrganisationSchema = new schema.Entity(spaceSchemaKey, { + entity: { + organization: OrganisationSchema + } +}, { + idAttribute: getAPIResourceGuid + }); diff --git a/src/frontend/app/store/actions/application.actions.ts b/src/frontend/app/store/actions/application.actions.ts index 3b2fd53de9..596ddb776e 100644 --- a/src/frontend/app/store/actions/application.actions.ts +++ b/src/frontend/app/store/actions/application.actions.ts @@ -5,7 +5,6 @@ import { Headers, RequestOptions, URLSearchParams } from '@angular/http'; import { schema } from 'normalizr'; import { ApiActionTypes } from './request.actions'; -import { SpaceSchema } from './space.actions'; import { StackSchema } from './stack.action'; import { ActionMergeFunction } from '../types/api.types'; import { PaginatedAction } from '../types/pagination.types'; @@ -14,6 +13,7 @@ import { pick } from '../helpers/reducer.helper'; import { AppMetadataTypes } from './app-metadata.actions'; import { AppStatSchema } from '../types/app-metadata.types'; import { getPaginationKey } from './pagination.actions'; +import { SpaceWithOrganisationSchema } from './action-types'; export const GET_ALL = '[Application] Get all'; export const GET_ALL_SUCCESS = '[Application] Get all success'; @@ -50,7 +50,7 @@ export const DELETE_INSTANCE_FAILED = '[Application Instance] Delete failed'; const ApplicationEntitySchema = { entity: { stack: StackSchema, - space: SpaceSchema + space: SpaceWithOrganisationSchema } }; diff --git a/src/frontend/app/store/actions/organisation.action.ts b/src/frontend/app/store/actions/organisation.action.ts deleted file mode 100644 index b59ddf576a..0000000000 --- a/src/frontend/app/store/actions/organisation.action.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CFStartAction, IRequestAction, ICFAction } from '../types/request.types'; -import { getAPIResourceGuid } from '../selectors/api.selectors'; -import { schema } from 'normalizr'; -import { ApiActionTypes } from './request.actions'; -import { RequestOptions } from '@angular/http'; - -export const GET = '[Organisation] Get one'; -export const GET_SUCCESS = '[Organisation] Get one success'; -export const GET_FAILED = '[Organisation] Get one failed'; - -export const OrganisationSchema = new schema.Entity('organization', {}, { - idAttribute: getAPIResourceGuid -}); - -export class GetOrganisation extends CFStartAction implements ICFAction { - constructor(public guid: string, public endpointGuid: string) { - super(); - this.options = new RequestOptions(); - this.options.url = `organization/${guid}`; - this.options.method = 'get'; - } - actions = [ - GET, - GET_SUCCESS, - GET_FAILED - ]; - entity = [OrganisationSchema]; - entityKey = OrganisationSchema.key; - options: RequestOptions; -} diff --git a/src/frontend/app/store/actions/organisation.actions.ts b/src/frontend/app/store/actions/organisation.actions.ts new file mode 100644 index 0000000000..b86166eb86 --- /dev/null +++ b/src/frontend/app/store/actions/organisation.actions.ts @@ -0,0 +1,52 @@ +import { RequestOptions } from '@angular/http'; + +import { PaginatedAction } from '../types/pagination.types'; +import { CFStartAction, ICFAction } from '../types/request.types'; +import { OrganisationSchema, organisationSchemaKey, OrganisationWithSpaceSchema } from './action-types'; + +export const GET_ORGANISATION = '[Organisation] Get one'; +export const GET_ORGANISATION_SUCCESS = '[Organisation] Get one success'; +export const GET_ORGANISATION_FAILED = '[Organisation] Get one failed'; + +export const GET_ORGANISATIONS = '[Organization] Get all'; +export const GET_ORGANISATIONS_SUCCESS = '[Organization] Get all success'; +export const GET_ORGANISATIONS_FAILED = '[Organization] Get all failed'; + +export class GetOrganisation extends CFStartAction implements ICFAction { + constructor(public guid: string, public endpointGuid: string) { + super(); + this.options = new RequestOptions(); + this.options.url = `organization/${guid}`; + this.options.method = 'get'; + } + actions = [ + GET_ORGANISATION, + GET_ORGANISATION_SUCCESS, + GET_ORGANISATION_FAILED + ]; + entity = [OrganisationSchema]; + entityKey = organisationSchemaKey; + options: RequestOptions; +} + +export class GetAllOrganisations extends CFStartAction implements PaginatedAction { + constructor(public paginationKey: string) { + super(); + this.options = new RequestOptions(); + this.options.url = 'organizations'; + this.options.method = 'get'; + } + actions = [ + GET_ORGANISATIONS, + GET_ORGANISATIONS_SUCCESS, + GET_ORGANISATIONS_FAILED + ]; + entity = [OrganisationWithSpaceSchema]; + entityKey = organisationSchemaKey; + options: RequestOptions; + initialParams = { + page: 1, + 'results-per-page': 100, + 'inline-relations-depth': 1 + }; +} diff --git a/src/frontend/app/store/actions/organization.actions.ts b/src/frontend/app/store/actions/organization.actions.ts deleted file mode 100644 index d02305b3c1..0000000000 --- a/src/frontend/app/store/actions/organization.actions.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { CFStartAction } from '../types/request.types'; -import { getAPIResourceGuid } from '../selectors/api.selectors'; -import { RequestOptions, URLSearchParams } from '@angular/http'; -import { schema } from 'normalizr'; - -import { ApiActionTypes } from './request.actions'; -import { SpaceSchema } from './space.actions'; -import { PaginatedAction } from '../types/pagination.types'; - -export const GET_ALL = '[Organization] Get all'; -export const GET_ALL_SUCCESS = '[Organization] Get all success'; -export const GET_ALL_FAILED = '[Organization] Get all failed'; - -export const OrganizationSchema = new schema.Entity('organization', { - entity: { - spaces: [SpaceSchema] - } -}, { - idAttribute: getAPIResourceGuid - }); - -export class GetAllOrganizations extends CFStartAction implements PaginatedAction { - constructor(public paginationKey: string) { - super(); - this.options = new RequestOptions(); - this.options.url = 'organizations'; - this.options.method = 'get'; - } - actions = [ - GET_ALL, - GET_ALL_SUCCESS, - GET_ALL_FAILED - ]; - entity = [OrganizationSchema]; - entityKey = OrganizationSchema.key; - options: RequestOptions; - initialParams = { - page: 1, - 'results-per-page': 100, - 'inline-relations-depth': 1 - }; -} diff --git a/src/frontend/app/store/actions/route.actions.ts b/src/frontend/app/store/actions/route.actions.ts index a6d9c8980b..645ad13c79 100644 --- a/src/frontend/app/store/actions/route.actions.ts +++ b/src/frontend/app/store/actions/route.actions.ts @@ -63,14 +63,14 @@ export class CreateRoute extends CFStartAction implements ICFAction { export class DeleteRoute extends CFStartAction implements ICFAction { constructor( - public routeGuid: string, + public guid: string, public cfGuid: string, public async: boolean = false, public recursive: boolean = true ) { super(); this.options = new RequestOptions(); - this.options.url = `routes/${routeGuid}`; + this.options.url = `routes/${guid}`; this.options.method = 'delete'; this.options.params = new URLSearchParams(); this.options.params.append('recursive', recursive ? 'true' : 'false'); @@ -86,6 +86,7 @@ export class DeleteRoute extends CFStartAction implements ICFAction { entityKey = RouteSchema.key; options: RequestOptions; endpointGuid: string; + removeEntityOnDelete = true; } export class UnmapRoute extends CFStartAction implements ICFAction { @@ -131,7 +132,6 @@ export class CheckRouteExists extends CFStartAction implements ICFAction { endpointGuid: string; } -// Refactor to satisfy CodeClimate export class ListRoutes extends CFStartAction implements PaginatedAction { constructor( public guid: string, @@ -151,10 +151,8 @@ export class ListRoutes extends CFStartAction implements PaginatedAction { entity = [RouteSchema]; entityKey = RouteSchema.key; options: RequestOptions; - initialParams = { - 'inline-relations-depth': '2' - }; endpointGuid: string; + flattenPagination = true; } export class GetAppRoutes extends ListRoutes implements PaginatedAction { @@ -166,8 +164,11 @@ export class GetAppRoutes extends ListRoutes implements PaginatedAction { ]); } initialParams = { - 'results-per-page': 9, // Match that of the page size used by the matching list config - 'inline-relations-depth': '1' + 'results-per-page': 100, + 'inline-relations-depth': '1', + page: 1, + 'order-direction': 'desc', + 'order-direction-field': 'route', }; } @@ -179,6 +180,13 @@ export class GetSpaceRoutes extends ListRoutes implements PaginatedAction { RouteEvents.GET_SPACE_ALL_FAILED ]); } + initialParams = { + 'results-per-page': 100, + 'inline-relations-depth': '1', + page: 1, + 'order-direction': 'desc', + 'order-direction-field': 'attachedApps', + }; } export class MapRouteSelected implements Action { diff --git a/src/frontend/app/store/actions/space.action.ts b/src/frontend/app/store/actions/space.action.ts deleted file mode 100644 index 2aaa481675..0000000000 --- a/src/frontend/app/store/actions/space.action.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { CFStartAction, IRequestAction, ICFAction } from '../types/request.types'; -import { getAPIResourceGuid } from '../selectors/api.selectors'; -import { schema } from 'normalizr'; -import { ApiActionTypes } from './request.actions'; -import { RequestOptions } from '@angular/http'; -import { OrganisationSchema } from './organisation.action'; - -export const GET = '[Space] Get one'; -export const GET_SUCCESS = '[Space] Get one success'; -export const GET_FAILED = '[Space] Get one failed'; - -export const SpaceSchema = new schema.Entity('space', { - entity: { - organization: OrganisationSchema - } -}, { - idAttribute: getAPIResourceGuid - }); - -export class GetSpace extends CFStartAction implements ICFAction { - constructor(public guid: string, public endpointGuid: string) { - super(); - this.options = new RequestOptions(); - this.options.url = `space/${guid}`; - this.options.method = 'get'; - } - actions = [ - GET, - GET_SUCCESS, - GET_FAILED - ]; - entity = [SpaceSchema]; - entityKey = SpaceSchema.key; - options: RequestOptions; -} diff --git a/src/frontend/app/store/actions/space.actions.ts b/src/frontend/app/store/actions/space.actions.ts index e108607a98..d6b7aa5d07 100644 --- a/src/frontend/app/store/actions/space.actions.ts +++ b/src/frontend/app/store/actions/space.actions.ts @@ -1,19 +1,32 @@ -import { - CFStartAction, - IRequestAction, - ICFAction -} from '../types/request.types'; -import { getAPIResourceGuid } from '../selectors/api.selectors'; import { RequestOptions, URLSearchParams } from '@angular/http'; -import { schema } from 'normalizr'; -import { ApiActionTypes } from './request.actions'; +import { CFStartAction, ICFAction } from '../types/request.types'; +import { SpaceSchema, spaceSchemaKey, SpaceWithOrganisationSchema } from './action-types'; -export const GET_ALL = '[Space] Get all'; -export const GET_ALL_SUCCESS = '[Space] Get all success'; -export const GET_ALL_FAILED = '[Space] Get all failed'; +export const GET_SPACES = '[Space] Get all'; +export const GET_SPACES_SUCCESS = '[Space] Get all success'; +export const GET_SPACES_FAILED = '[Space] Get all failed'; -export const SpaceSchema = new schema.Entity('space'); +export const GET_SPACE = '[Space] Get one'; +export const GET_SPACE_SUCCESS = '[Space] Get one success'; +export const GET_SPACE_FAILED = '[Space] Get one failed'; + +export class GetSpace extends CFStartAction implements ICFAction { + constructor(public guid: string, public endpointGuid: string) { + super(); + this.options = new RequestOptions(); + this.options.url = `space/${guid}`; + this.options.method = 'get'; + } + actions = [ + GET_SPACE, + GET_SPACE_SUCCESS, + GET_SPACE_FAILED + ]; + entity = [SpaceSchema]; + entityKey = spaceSchemaKey; + options: RequestOptions; +} export class GetAllSpaces extends CFStartAction implements ICFAction { constructor(public paginationKey?: string) { @@ -26,8 +39,8 @@ export class GetAllSpaces extends CFStartAction implements ICFAction { this.options.params.set('results-per-page', '100'); this.options.params.set('inline-relations-depth', '1'); } - actions = [GET_ALL, GET_ALL_SUCCESS, GET_ALL_FAILED]; - entity = [SpaceSchema]; - entityKey = SpaceSchema.key; + actions = [GET_SPACES, GET_SPACES_SUCCESS, GET_SPACES_FAILED]; + entity = [SpaceWithOrganisationSchema]; + entityKey = spaceSchemaKey; options: RequestOptions; } diff --git a/src/frontend/app/store/effects/endpoint.effects.ts b/src/frontend/app/store/effects/endpoint.effects.ts index 174d565146..00d9b30f27 100644 --- a/src/frontend/app/store/effects/endpoint.effects.ts +++ b/src/frontend/app/store/effects/endpoint.effects.ts @@ -40,6 +40,7 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { SystemInfo } from '../types/system.types'; import { map, mergeMap, catchError } from 'rxjs/operators'; import { GetSystemInfo, GET_SYSTEM_INFO, GET_SYSTEM_INFO_SUCCESS, GetSystemSuccess } from '../actions/system.actions'; +import { ClearPaginationOfType, ClearPaginationOfEntity } from '../actions/pagination.actions'; @Injectable() export class EndpointsEffect { @@ -47,7 +48,6 @@ export class EndpointsEffect { static connectingKey = 'connecting'; static disconnectingKey = 'disconnecting'; static registeringKey = 'registering'; - static unregisteringKey = 'unregistering'; constructor( private http: HttpClient, @@ -91,7 +91,7 @@ export class EndpointsEffect { @Effect() connectEndpoint$ = this.actions$.ofType(CONNECT_ENDPOINTS) .flatMap(action => { const actionType = 'update'; - const apiAction = this.getEndpointAction(action.guid, action.type, EndpointsEffect.connectingKey); + const apiAction = this.getEndpointUpdateAction(action.guid, action.type, EndpointsEffect.connectingKey); const params: HttpParams = new HttpParams({ fromObject: { 'cnsi_guid': action.guid, @@ -112,7 +112,7 @@ export class EndpointsEffect { @Effect() disconnect$ = this.actions$.ofType(DISCONNECT_ENDPOINTS) .flatMap(action => { - const apiAction = this.getEndpointAction(action.guid, action.type, EndpointsEffect.disconnectingKey); + const apiAction = this.getEndpointUpdateAction(action.guid, action.type, EndpointsEffect.disconnectingKey); const params: HttpParams = new HttpParams({ fromObject: { 'cnsi_guid': action.guid @@ -131,7 +131,7 @@ export class EndpointsEffect { @Effect() unregister$ = this.actions$.ofType(UNREGISTER_ENDPOINTS) .flatMap(action => { - const apiAction = this.getEndpointAction(action.guid, action.type, EndpointsEffect.unregisteringKey); + const apiAction = this.getEndpointDeleteAction(action.guid, action.type); const params: HttpParams = new HttpParams({ fromObject: { 'cnsi_guid': action.guid @@ -150,7 +150,7 @@ export class EndpointsEffect { @Effect() register$ = this.actions$.ofType(REGISTER_ENDPOINTS) .flatMap(action => { - const apiAction = this.getEndpointAction(action.guid(), action.type, EndpointsEffect.registeringKey); + const apiAction = this.getEndpointUpdateAction(action.guid(), action.type, EndpointsEffect.registeringKey); const params: HttpParams = new HttpParams({ fromObject: { 'cnsi_name': action.name, @@ -168,7 +168,8 @@ export class EndpointsEffect { ); }); - private getEndpointAction(guid, type, updatingKey) { + + private getEndpointUpdateAction(guid, type, updatingKey) { return { entityKey: endpointStoreNames.type, guid, @@ -177,6 +178,14 @@ export class EndpointsEffect { } as IRequestAction; } + private getEndpointDeleteAction(guid, type) { + return { + entityKey: endpointStoreNames.type, + guid, + type, + } as IRequestAction; + } + private doEndpointAction( apiAction: IRequestAction, url: string, @@ -194,6 +203,9 @@ export class EndpointsEffect { if (actionStrings[0]) { this.store.dispatch({ type: actionStrings[0] }); } + if (apiActionType === 'delete') { + this.store.dispatch(new ClearPaginationOfEntity(apiAction.entityKey, apiAction.guid)); + } return new WrapperRequestActionSuccess(null, apiAction, apiActionType); }) .catch(e => { diff --git a/src/frontend/sass/_all-theme.scss b/src/frontend/sass/_all-theme.scss index a1bb56bbeb..dfb2597c9b 100644 --- a/src/frontend/sass/_all-theme.scss +++ b/src/frontend/sass/_all-theme.scss @@ -47,25 +47,15 @@ $side-nav-light-active: #484848; } html { background-color: $app-background-color; - } - - // App Theme defines a set of colors used by stratos components - $app-theme: ( - app-background-color: $app-background-color, - app-background-text-color: rgba(mat-color($foreground-colors, base), .65), - side-nav: app-generate-nav-theme($theme, $nav-theme), - status: app-generate-status-theme($theme, $status-theme), - subdued-color: $subdued - ); - - // Pass the Material theme and the App Theme to components that need to be themed + } // App Theme defines a set of colors used by stratos components + $app-theme: (app-background-color: $app-background-color, app-background-text-color: rgba(mat-color($foreground-colors, base), .65), side-nav: app-generate-nav-theme($theme, $nav-theme), status: app-generate-status-theme($theme, $status-theme), subdued-color: $subdued); // Pass the Material theme and the App Theme to components that need to be themed @include dialog-error-theme($theme, $app-theme); @include login-page-theme($theme, $app-theme); @include side-nav-theme($theme, $app-theme); @include dashboard-page-theme($theme, $app-theme); @include display-value-theme($theme, $app-theme); @include steppers-theme($theme, $app-theme); - @include variables-tab-theme($theme, $app-theme); + @include list-theme($theme, $app-theme); @include app-base-page-theme($theme, $app-theme); @include app-page-subheader-theme($theme, $app-theme); @include app-mat-tabs-theme($theme, $app-theme);