diff --git a/.gitignore b/.gitignore index f5a3d9b93c..ea489df810 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # compiled output /dist +.dist /tmp /out-tsc /out @@ -56,6 +57,8 @@ lib tools/test-backend/config/mock.config.json tools/.coverage-karma/ dev-certs/* +vendor/ +**/vendor **/*.so .glide tools/ssl diff --git a/deploy/build_portal_proxy.sh b/deploy/build_portal_proxy.sh index cbc7cd7ed7..7df19089f1 100755 --- a/deploy/build_portal_proxy.sh +++ b/deploy/build_portal_proxy.sh @@ -12,9 +12,9 @@ docker run -it \ -e USER_NAME=$(id -nu) \ -e USER_ID=$(id -u) \ -e GROUP_ID=$(id -g) \ - --name console-proxy-builder \ + --name stratos-jetstream-builder \ --volume $(pwd):/go/src/github.com/SUSE/stratos-ui \ - splatform/stratos-proxy-builder:opensuse $* + splatform/stratos-jetstream-builder:opensuse $* ret=$? popd diff --git a/deploy/ci/tasks/stratos-ui/prep-proxy-image.yml b/deploy/ci/tasks/stratos-ui/prep-proxy-image.yml index e0132e97b0..ca5ca3fd93 100644 --- a/deploy/ci/tasks/stratos-ui/prep-proxy-image.yml +++ b/deploy/ci/tasks/stratos-ui/prep-proxy-image.yml @@ -3,7 +3,7 @@ platform: linux image_resource: type: docker-image source: - repository: ci-registry.capbristol.com:5000/splatform/stratos-proxy-builder + repository: ci-registry.capbristol.com:5000/splatform/stratos-jetstream-builder tag: "opensuse" insecure_registries: [ "ci-registry.capbristol.com:5000" ] @@ -17,7 +17,7 @@ run: - -exc - | cd stratos-ui - npm install --production + npm install npm run build-backend cd - cp -r ./stratos-ui/outputs ./portal-proxy-output diff --git a/deploy/db/scripts/development.sh b/deploy/db/scripts/development.sh index 615da17bae..d8b03ec927 100755 --- a/deploy/db/scripts/development.sh +++ b/deploy/db/scripts/development.sh @@ -20,7 +20,7 @@ echo "Checking database status." # Run migrations echo "Attempting database migrations." -./portal-proxy--env=mariadb-development up +./portal-proxy --env=mariadb-development up # CHeck the status echo "Checking database status." diff --git a/deploy/docker-compose/build.sh b/deploy/docker-compose/build.sh index 3a4d4d411a..851cff3419 100755 --- a/deploy/docker-compose/build.sh +++ b/deploy/docker-compose/build.sh @@ -205,9 +205,9 @@ function buildProxy { -e USER_NAME=$(id -nu) \ -e USER_ID=$(id -u) \ -e GROUP_ID=$(id -g) \ - --name stratos-proxy-builder \ + --name stratos-jetstream-builder \ --volume $(pwd):/go/src/github.com/SUSE/stratos-ui \ - ${DOCKER_REGISTRY}/${DOCKER_ORG}/stratos-proxy-builder:opensuse + ${DOCKER_REGISTRY}/${DOCKER_ORG}/stratos-jetstream-builder:opensuse popd > /dev/null 2>&1 popd > /dev/null 2>&1 diff --git a/deploy/kubernetes/build.sh b/deploy/kubernetes/build.sh index 012890766e..715fc4e335 100755 --- a/deploy/kubernetes/build.sh +++ b/deploy/kubernetes/build.sh @@ -214,10 +214,10 @@ function buildProxy { # Use the existing build container to compile the proxy executable, and leave # it on the local filesystem. echo - echo "-- Building the Console Proxy" + echo "-- Building the Stratos Backend" echo - echo "-- Run the build container to build the Console backend" + echo "-- Run the build container to build the Stratos backend" pushd ${STRATOS_UI_PATH} > /dev/null 2>&1 pushd $(git rev-parse --show-toplevel) > /dev/null 2>&1 @@ -229,9 +229,9 @@ function buildProxy { -e USER_NAME=$(id -nu) \ -e USER_ID=$(id -u) \ -e GROUP_ID=$(id -g) \ - --name stratos-proxy-builder \ + --name stratos-jetstream-builder \ --volume $(pwd):/go/src/github.com/SUSE/stratos-ui \ - ${DOCKER_REGISTRY}/${DOCKER_ORG}/stratos-proxy-builder:${BASE_IMAGE_TAG} + ${DOCKER_REGISTRY}/${DOCKER_ORG}/stratos-jetstream-builder:${BASE_IMAGE_TAG} popd > /dev/null 2>&1 popd > /dev/null 2>&1 @@ -254,9 +254,9 @@ function buildPostflightJob { -e USER_NAME=$(id -nu) \ -e USER_ID=$(id -u) \ -e GROUP_ID=$(id -g) \ - --name stratos-proxy-builder \ + --name stratos-jetstream-builder \ --volume $(pwd):/go/src/github.com/SUSE/stratos-ui \ - ${DOCKER_REGISTRY}/${DOCKER_ORG}/stratos-proxy-builder:${BASE_IMAGE_TAG} + ${DOCKER_REGISTRY}/${DOCKER_ORG}/stratos-jetstream-builder:${BASE_IMAGE_TAG} buildAndPublishImage stratos-postflight-job deploy/db/Dockerfile.k8s.postflight-job ${STRATOS_UI_PATH} popd > /dev/null 2>&1 diff --git a/deploy/stand-up-dev-env.sh b/deploy/stand-up-dev-env.sh index 4c1b76432d..407dea5fbf 100755 --- a/deploy/stand-up-dev-env.sh +++ b/deploy/stand-up-dev-env.sh @@ -71,6 +71,13 @@ function uaa_downloads { ./uaa/prepare.sh } +function dev_certs { + CERTS_PATH="${PROG_DIR}/../dev-certs" + if [ ! -d "${CERTS_PATH}" ]; then + CERTS_PATH=${CERTS_PATH} ./tools/generate_cert.sh + fi +} + function build { echo "===== Building the portal proxy" export USER_ID=$(id -u) @@ -131,6 +138,7 @@ if [ "$CLEAN" = true ] ; then clean fi uaa_downloads +dev_certs build docker ps popd diff --git a/deploy/stratos-base-images/build-base-images.sh b/deploy/stratos-base-images/build-base-images.sh index a9d7c4ee90..beb40d4c2e 100755 --- a/deploy/stratos-base-images/build-base-images.sh +++ b/deploy/stratos-base-images/build-base-images.sh @@ -127,7 +127,7 @@ build_ui_base; build_bk_base; # Used for hosting nginx build_nginx_base; -# Used for stratos-proxy-builder base +# Used for stratos-jetstream-builder base build_bk_build_base; # Used for building the backend build_portal_proxy_builder; diff --git a/deploy/tools/build-push-proxy-builder-image.sh b/deploy/tools/build-push-proxy-builder-image.sh index f5467c9409..e2b004f7ab 100755 --- a/deploy/tools/build-push-proxy-builder-image.sh +++ b/deploy/tools/build-push-proxy-builder-image.sh @@ -46,7 +46,7 @@ else echo " REGISTRY: ${DOCKER_REGISTRY}" echo " ORG: ${DOCKER_ORG}" fi -NAME=stratos-proxy-builder +NAME=stratos-jetstream-builder TAG=${TAG:-opensuse} BK_BUILD_BASE=${BK_BUILD_BASE:-splatform/stratos-bk-build-base:opensuse} diff --git a/docs/planning/cf-user-management.md b/docs/planning/cf-user-management.md new file mode 100644 index 0000000000..0a7be4a14f --- /dev/null +++ b/docs/planning/cf-user-management.md @@ -0,0 +1,83 @@ +# Cloud Foundry User Management + +## Requirements + +* View the organisation and space roles for all users +* Edit the organisation and space roles for a specific user +* Assign multiple organisation and/or space roles for multiple users + +## V1 + +* Users tab + table at CF, organisation and space level + * CF level shows user _organisation_ roles for all organisations as pills + * Org level shows user _space_ roles for all spaces as pills + * Space level shows user _space_ roles for space as pills +* Table provided links to update roles + * 'Manage' a single user - pop up showing orgs/spaces (depending on level) roles. Allowed edit of all shown roles + * 'Change' multiple users - similar to manage, however no existing roles were shown. New selection _replaced_ existing roles + * 'Remove all' roles of selected users. These are specific to the level (cf - remove all orgs/spaces, org - remove all org/spaces, space - remove all space) +* All cf/org/space pages allow user to 'Assign' roles in a pop up + * pop up contains stepper, one stage to select user/s and another role/s + * can only assign roles to a single org and it's spaces + * no existing roles are shown + +### V1 Issues + +* Handling large amount of orgs or spaces + * Pill format to represent user roles lead to potentially large blobs of pills which were hard to extract information from but did show + the data in as small as possible area + * The Manage/Change popup didn't scale well at the CF level for lots of orgs or org level for lots of spaces +* Only showing org or space roles in the table can lead to confusion, for example user edits org roles at space level and no changes to table. +* Multiple ways to reach the same window (buttons above tables, row actions) +* Can only mass assign roles one org at a time +* No concept of inviting users + * Need to understand how this would work by supplying an email. API provides a way to create users with their UAA guid + +## V2 + +### First pass implementation - Changes to V1 + +* 'Change' multiple user modal + * To be removed. The ability to 'reset' multiple users needs more work. +* 'Manage' single user modal + * Only allow edit of a single org and it's spaces in the 'Manage' pop up, as per the assign. This restricts functionality but presents the + information in a clearer way and scales much better for multiple orgs. + * Tidy the position of 'remove from org' + * Stretch - Remove the 'org user' and handle automatically in the background? +* 'Assign' + * Step 2 - Ensure consistent UX with 'Manage' or vice versa +* Users Tab/Table + * Show org and space columns for their roles. Relevant to level (cf - both, org - both, space - space only) + * Provide a way to collapse list of roles automatically when there's a large amount. + * Stretch - Provide a way to filter per role. This will allow user to quickly see who's, for instance, a manager. + +### Second pass + +* Invite/Create user +* Remove user +* Reset users roles + +### Design input required pre release + +* Validate first pass approach +* Review use case/possible solutions to update/reset multiple users roles +* Review use of pills in table +* Review second roles column in table +* Review Manage/Assign layout + +## Similar Implementations + +High level description of other CF UIs + +* Management at org and space level + * Roughly table like views + * Each user listed in rows + * Org/Space roles as columns +* Editing a users roles by.. + * a check box in the roles column (each change an individual api request at time of click) + * a pop up allowing edits to all org OR space roles for a specific row/user + * All edits are very specific to user +* Provide a way to invite new users by email address. + * Can specify what roles they have at invite time + +>> Note .. There's no easy way to check user/s management at Cloud Foundry admin level, which is where some of the fun starts. diff --git a/src/backend/app-core/auth.go b/src/backend/app-core/auth.go index 0740e2efd0..9589bc93cd 100644 --- a/src/backend/app-core/auth.go +++ b/src/backend/app-core/auth.go @@ -212,19 +212,19 @@ func (p *portalProxy) fetchToken(cnsiGUID string, c echo.Context) (*UAAResponse, } authTypeStr := c.FormValue("auth") - authType := interfaces.OAuth2 + authType := interfaces.AuthTypeOAuth2 switch authTypeStr { case "http": - authType = interfaces.HttpBasic + authType = interfaces.AuthTypeHttpBasic default: - authType = interfaces.OAuth2 + authType = interfaces.AuthTypeOAuth2 } - if authType == interfaces.OAuth2 { + if authType == interfaces.AuthTypeOAuth2 { return p.fetchOAuth2Token(cnsiRecord, c) } - if authType == interfaces.HttpBasic { + if authType == interfaces.AuthTypeHttpBasic { return p.fetchHttpBasicToken(cnsiRecord, c) } diff --git a/deploy/db/migrations/20171108102900_AuthType.go b/src/backend/app-core/datastore/20171108102900_AuthType.go similarity index 61% rename from deploy/db/migrations/20171108102900_AuthType.go rename to src/backend/app-core/datastore/20171108102900_AuthType.go index 7ce1609602..fff38d3332 100644 --- a/deploy/db/migrations/20171108102900_AuthType.go +++ b/src/backend/app-core/datastore/20171108102900_AuthType.go @@ -1,21 +1,23 @@ -package main +package datastore import ( "database/sql" "fmt" ) -func Up_20171108102900(txn *sql.Tx) { - - createTokens := "ALTER TABLE tokens " - createTokens += "ADD auth_type VARCHAR(255) DEFAULT \"OAuth2\", " - createTokens += "ADD meta_data TEXT " - createTokens += ";" +func (s *StratosMigrations) Up_20171108102900(txn *sql.Tx) { + createTokens := "ALTER TABLE tokens ADD auth_type VARCHAR(255) DEFAULT \"OAuth2\"" _, err := txn.Exec(createTokens) if err != nil { fmt.Printf("Failed to migrate due to: %v", err) } + + createTokens = "ALTER TABLE tokens ADD meta_data TEXT" + _, err = txn.Exec(createTokens) + if err != nil { + fmt.Printf("Failed to migrate due to: %v", err) + } } func Down_20171108102900(txn *sql.Tx) { diff --git a/src/backend/app-core/repository/interfaces/structs.go b/src/backend/app-core/repository/interfaces/structs.go index 521c1b9b8e..a9ea833bc4 100644 --- a/src/backend/app-core/repository/interfaces/structs.go +++ b/src/backend/app-core/repository/interfaces/structs.go @@ -31,11 +31,9 @@ type CNSIRecord struct { SkipSSLValidation bool `json:"skip_ssl_validation"` } -type AuthType string - const ( - OAuth2 AuthType = "OAuth2" - HttpBasic AuthType = "HttpBasic" + AuthTypeOAuth2 = "OAuth2" + AuthTypeHttpBasic = "HttpBasic" ) //TODO this could be moved back to tokens subpackage, and extensions could import it? @@ -44,7 +42,7 @@ type TokenRecord struct { RefreshToken string TokenExpiry int64 Disconnected bool - AuthType AuthType + AuthType string Metadata string } diff --git a/src/backend/app-core/repository/tokens/pgsql_tokens.go b/src/backend/app-core/repository/tokens/pgsql_tokens.go index cad032e27f..4a8c15aa5b 100644 --- a/src/backend/app-core/repository/tokens/pgsql_tokens.go +++ b/src/backend/app-core/repository/tokens/pgsql_tokens.go @@ -46,8 +46,8 @@ var insertCNSIToken = `INSERT INTO tokens (cnsi_guid, user_guid, token_type, aut VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)` var updateCNSIToken = `UPDATE tokens - SET auth_token = $1, refresh_token = $2, token_expiry = $3, disconnected = $4 - WHERE cnsi_guid = $5 AND user_guid = $6 AND token_type = $7 AND auth_type = $7 AND meta_data = $8` + SET auth_token = $1, refresh_token = $2, token_expiry = $3, disconnected = $4, meta_data = $5, + WHERE cnsi_guid = $6 AND user_guid = $7 AND token_type = $8 AND auth_type = $9` var deleteCNSIToken = `DELETE FROM tokens WHERE token_type = 'cnsi' AND cnsi_guid = $1 AND user_guid = $2` @@ -238,7 +238,7 @@ func (p *PgsqlTokenRepository) SaveCNSIToken(cnsiGUID string, userGUID string, t case 0: if _, insertErr := p.db.Exec(insertCNSIToken, cnsiGUID, userGUID, "cnsi", ciphertextAuthToken, - ciphertextRefreshToken, tr.TokenExpiry, tr.Disconnected, tr.AuthType, tr.AuthType, tr.Metadata); insertErr != nil { + ciphertextRefreshToken, tr.TokenExpiry, tr.Disconnected, tr.AuthType, tr.Metadata); insertErr != nil { msg := "Unable to INSERT CNSI token: %v" log.Printf(msg, insertErr) @@ -251,7 +251,7 @@ func (p *PgsqlTokenRepository) SaveCNSIToken(cnsiGUID string, userGUID string, t log.Println("Existing CNSI token found - attempting update.") result, err := p.db.Exec(updateCNSIToken, ciphertextAuthToken, ciphertextRefreshToken, tr.TokenExpiry, - tr.Disconnected, cnsiGUID, userGUID, "cnsi", tr.AuthType, tr.Metadata) + tr.Disconnected, tr.Metadata, cnsiGUID, userGUID, "cnsi", tr.AuthType) if err != nil { msg := "Unable to UPDATE CNSI token: %v" log.Printf(msg, err) diff --git a/src/frontend/app/core/entity-service.ts b/src/frontend/app/core/entity-service.ts index 56a108d270..82d6cbb062 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-instances/card-app-instances.component.html b/src/frontend/app/shared/components/cards/card-app-instances/card-app-instances.component.html index 526d47b0ce..28f4af150a 100644 --- a/src/frontend/app/shared/components/cards/card-app-instances/card-app-instances.component.html +++ b/src/frontend/app/shared/components/cards/card-app-instances/card-app-instances.component.html @@ -1,5 +1,5 @@ - + Instances 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 f96477add8..bc3e40d780 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 @@ -7,7 +7,7 @@ import { OperatorFunction } from 'rxjs/interfaces'; import { Observable } from 'rxjs/Observable'; import { combineLatest } from 'rxjs/observable/combineLatest'; import { distinctUntilChanged } from 'rxjs/operators'; -import { map, shareReplay } from 'rxjs/operators'; +import { map, shareReplay, publish, refCount, publishReplay, share, filter } from 'rxjs/operators'; import { Subscription } from 'rxjs/Subscription'; import { SetResultCount } from '../../../../store/actions/pagination.actions'; @@ -113,8 +113,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); } }); @@ -242,25 +247,35 @@ export abstract class ListDataSource extends DataSource implements pagination$, page$ ).pipe( + 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') - ); + ); } getPaginationCompareString(paginationEntity: PaginationEntityState) { 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 65096584f6..f24f5ca69c 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'; @@ -29,9 +28,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', @@ -55,7 +53,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()); }); @@ -151,12 +149,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..14e9a58caf 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,60 @@ &__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 { + 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..750a534718 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,26 @@ @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); + } + mat-card { + flex-direction: column; + @include breakpoint(desktop) { + flex-direction: row; + } + } + &__right { + justify-content: flex-start; + @include breakpoint(desktop) { + justify-content: flex-end; + } + } + } } } diff --git a/src/frontend/app/shared/components/list/list.component.ts b/src/frontend/app/shared/components/list/list.component.ts index d2a2bc5856..67147cfccb 100644 --- a/src/frontend/app/shared/components/list/list.component.ts +++ b/src/frontend/app/shared/components/list/list.component.ts @@ -27,6 +27,8 @@ import { IMultiListAction, ListConfig, } from './list.component.types'; +import { combineLatest } from 'rxjs/observable/combineLatest'; +import { map } from 'rxjs/operators'; @Component({ @@ -62,6 +64,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] @@ -82,11 +87,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.config.defaultView || 'table'); @@ -96,6 +110,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 6f53b78a65..958137025b 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);