diff --git a/.cfignore b/.cfignore index 21c16f20e7..92be10a235 100644 --- a/.cfignore +++ b/.cfignore @@ -21,3 +21,4 @@ deploy/uaa/ docs/ build/dev_config.json e2e-reports/ +website/ diff --git a/.gitignore b/.gitignore index d8aa597f69..b60b3f5527 100644 --- a/.gitignore +++ b/.gitignore @@ -123,4 +123,4 @@ go-vendor-*.tgz website/build website/site-dist -website/.docusaurus \ No newline at end of file +website/.docusaurus diff --git a/CHANGELOG.md b/CHANGELOG.md index a998a95757..25b531ccc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,76 @@ # Change Log +## 4.0.0 + +[Full Changelog](https://github.com/cloudfoundry/stratos/compare/3.2.1...4.0.0) + +This release contains a number of fixes and improvements: + +**Improvements:** + +- Extensions: Allow typed access to store entities and their actions [\#4494](https://github.com/cloudfoundry/stratos/issues/4494) +- Extensions: Remove the need for symlinks and improve the build process [\#4472](https://github.com/cloudfoundry/stratos/issues/4472) +- Extensions: Allow Themes to be published and installed to/from npm [\#4471](https://github.com/cloudfoundry/stratos/issues/4471) +- Extensions: Move to extensions and themes to be packages [\#4470](https://github.com/cloudfoundry/stratos/issues/4470) +- Show service broker space scope information in service wall list [\#4458](https://github.com/cloudfoundry/stratos/issues/4458) +- Client Secret is shown in the clear in the UI [\#4445](https://github.com/cloudfoundry/stratos/issues/4445) +- Improve sizing of UI elements on desktop browsers [\#4419](https://github.com/cloudfoundry/stratos/issues/4419) +- Theming: Allow more control over link and side navigation colors [\#4406](https://github.com/cloudfoundry/stratos/issues/4406) +- Update to latest set of icons [\#4403](https://github.com/cloudfoundry/stratos/issues/4403) +- Theming: Allow more control over page header colors and style [\#4396](https://github.com/cloudfoundry/stratos/issues/4396) +- Helm Chart: Remove encryption volume [\#4351](https://github.com/cloudfoundry/stratos/issues/4351) +- Improve app summary responsiveness [\#4348](https://github.com/cloudfoundry/stratos/issues/4348) +- Improve UI for the case when we can't determine cf app deployment info [\#4347](https://github.com/cloudfoundry/stratos/issues/4347) +- Change recent activity icon to avoid confusion with the refresh button [\#4346](https://github.com/cloudfoundry/stratos/issues/4346) +- Helm Chart: Change default image pull policy to Always [\#4342](https://github.com/cloudfoundry/stratos/issues/4342) +- Permissions: Org Managers: Disable org role checkboxes in roles stepper if not admin/org manager [\#4332](https://github.com/cloudfoundry/stratos/issues/4332) +- Autoscaler: Add support for custom metrics [\#4298](https://github.com/cloudfoundry/stratos/issues/4298) +- Add support for copying endpoint address in list view [\#4238](https://github.com/cloudfoundry/stratos/issues/4238) +- Update to Angular 9 framework [\#4214](https://github.com/cloudfoundry/stratos/issues/4214) +- Update docker logo in deploy app stepper [\#4133](https://github.com/cloudfoundry/stratos/issues/4133) +- Helm Chart: Remove need for --recreate-pods when upgrading [\#4132](https://github.com/cloudfoundry/stratos/issues/4132) +- Make permissions model extension friendly [\#3789](https://github.com/cloudfoundry/stratos/issues/3789) +- User Favourites: Add icons to cards [\#3409](https://github.com/cloudfoundry/stratos/issues/3409) +- Improve log out experience [\#2587](https://github.com/cloudfoundry/stratos/issues/2587) + +**Fixes:** + +- Ensure `cf push` works from Windows [\#4465](https://github.com/cloudfoundry/stratos/issues/4465) +- SSLMode is not respected for database connections [\#4434](https://github.com/cloudfoundry/stratos/issues/4434) +- Visiting marketplace tab breaks service list [\#4397](https://github.com/cloudfoundry/stratos/issues/4397) +- CF Application reports error after restage [\#4392](https://github.com/cloudfoundry/stratos/issues/4392) +- Helm Chart: Icon is missing [\#4370](https://github.com/cloudfoundry/stratos/issues/4370) +- Permissions: Users with no developer roles can click on create app button [\#4361](https://github.com/cloudfoundry/stratos/issues/4361) +- Edit endpoint not available in table view [\#4349](https://github.com/cloudfoundry/stratos/issues/4349) +- CF: Routes List: Filter by org breaks when user is an org auditor [\#4343](https://github.com/cloudfoundry/stratos/issues/4343) +- Permissions: Only space developers should be able to see add service instance buttons [\#4331](https://github.com/cloudfoundry/stratos/issues/4331) +- Permissions: Only Space Developers should be able to change count, terminate or ssh to instances [\#4330](https://github.com/cloudfoundry/stratos/issues/4330) +- App: Gitlab Tab: Fix console errors [\#4325](https://github.com/cloudfoundry/stratos/issues/4325) +- Permissions: Only space developers should be able to create/unbind/delete routes in app routes list [\#4324](https://github.com/cloudfoundry/stratos/issues/4324) +- Permissions: Only Space Developers should be able to create/edit/delete an Autoscaler policy [\#4323](https://github.com/cloudfoundry/stratos/issues/4323) +- Permissions: Only space developers should be able to see the app summary deployment card [\#4322](https://github.com/cloudfoundry/stratos/issues/4322) +- Duplicated documentation between deploy/kubernetes and Helm Chart README.md [\#4315](https://github.com/cloudfoundry/stratos/issues/4315) +- App Service Edit Binding: cancel of stepper results in leaked subscription [\#4295](https://github.com/cloudfoundry/stratos/issues/4295) +- Exceptions thrown when navigating back from marketplace [\#4287](https://github.com/cloudfoundry/stratos/issues/4287) +- Unbind services stepper fails to show bound services [\#4246](https://github.com/cloudfoundry/stratos/issues/4246) +- Progress icon appears too close to right-hand size of table [\#4234](https://github.com/cloudfoundry/stratos/issues/4234) +- Cancel/create in service instance stepper returns to incorrect locations [\#4052](https://github.com/cloudfoundry/stratos/issues/4052) +- Exception thrown in setup steppers [\#3897](https://github.com/cloudfoundry/stratos/issues/3897) +- Logout leaves the UI as is if the verify or logout call fails [\#2633](https://github.com/cloudfoundry/stratos/issues/2633) +- Cf Build Packs: file name should wrap to next line if too long [\#1803](https://github.com/cloudfoundry/stratos/issues/1803) + +**Breaking Changes:** + +- **Customizations in `custom-src` are now npm packages** + + Stratos customizations were previously in `./custom-src` and included in the build via symlinks. These customizations have now moved into local npm packages located in `./src/frontend/packages`. For more details please see our customization documentation at `./website/docs/extensions/introduction.md` or `https://stratos.app/docs/extensions/introduction`. There you will also find instructions on migrating to npm packages and a tool to help automate most of the process. +- **Kubernetes: Upgrade only possible from version 3.0.0 or later** + + When deploying into Kubernetes using Helm and upgrading from an earlier version of Stratos using `helm upgrade`, upgrade is **only** supported from version 3.0.0 or later. If you are using an earlier version, first upgrade to version 3.x before then upgrading to the latest version. +- **Angular 9 requires extensions to be declared** + + Extension components must now be made known to the extensions system in the module that they are declared in, using `ExtensionService.declare`. Please check the documentation. This is required to ensure that the new Angular compiler for Ivy does not remove these components for being unreferenced in the application. + ## 3.2.1 [Full Changelog](https://github.com/cloudfoundry/stratos/compare/3.2.0...3.2.1) diff --git a/README.md b/README.md index 397d3aaabd..a234c26d99 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Take a look at the [Feature Set](docs/features.md) for details on the feature se Get an [Overview](docs/overview.md) of Stratos, its components and the different ways in which it can be deployed. -Take a look at the [Development Roadmap](docs/roadmap.md) to see where we are heading. We update our status page each week to summarize what we are working on - see the [Status Page](docs/status_updates.md). +Take a look at the [Development Roadmap](docs/roadmap.md) to see where we are heading. Browse through features and issues in the project's [issues](https://github.com/cloudfoundry/stratos/issues) page. diff --git a/docs/issue_template.md b/docs/issue_template.md index b7db21a17a..c8b8913393 100644 --- a/docs/issue_template.md +++ b/docs/issue_template.md @@ -1,10 +1,9 @@ ---- -id: issue_template -title: Issue report template -sidebar_label: Issue report template ---- + +### Stratos Version + + + - ### Frontend Deployment type @@ -35,7 +34,7 @@ Insert log hereCopy ``` - + ### Detailed Description diff --git a/package-lock.json b/package-lock.json index 33629585bd..fc2dcd1098 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "stratos", - "version": "3.2.1", + "version": "4.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e66085d996..ef28212ea8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stratos", - "version": "3.2.1", + "version": "4.0.0", "description": "Stratos Console", "main": "index.js", "scripts": { diff --git a/src/frontend/packages/core/src/shared/components/file-input/file-input.component.scss b/src/frontend/packages/core/src/shared/components/file-input/file-input.component.scss index b1417ebcc8..aa0e1b908a 100644 --- a/src/frontend/packages/core/src/shared/components/file-input/file-input.component.scss +++ b/src/frontend/packages/core/src/shared/components/file-input/file-input.component.scss @@ -7,6 +7,7 @@ pointer-events: none; } &__field { + align-items: center; display: flex; } &__input { diff --git a/src/frontend/packages/store/src/actions/entity.delete.actions.ts b/src/frontend/packages/store/src/actions/entity.delete.actions.ts new file mode 100644 index 0000000000..2b69ee601b --- /dev/null +++ b/src/frontend/packages/store/src/actions/entity.delete.actions.ts @@ -0,0 +1,31 @@ +import { Action } from '@ngrx/store'; + +import { EntityRequestAction } from '../types/request.types'; +import { IFavoriteMetadata, UserFavorite } from '../types/user-favorites.types'; + +export class EntityDeleteCompleteAction implements Action { + + public static ACTION_TYPE = '[Entity] Entity delete complete'; + public type = EntityDeleteCompleteAction.ACTION_TYPE; + + constructor( + public entityGuid: string, + public entityType: string, + public endpointGuid: string, + public endpointType: string, + public action: EntityRequestAction, + ) {} + + // Create an entity delete action if we have all of the properties we need + public static parse(action: EntityRequestAction): EntityDeleteCompleteAction { + if (action.guid && action.entityType && action.endpointType && action.endpointGuid) { + return new EntityDeleteCompleteAction(action.guid, action.entityType, action.endpointGuid, action.endpointType, action); + } + return null; + } + + public asFavorite(): UserFavorite { + return new UserFavorite(this.endpointGuid, this.endpointType, this.entityType, this.entityGuid); + } + +} diff --git a/src/frontend/packages/store/src/effects/api.effects.ts b/src/frontend/packages/store/src/effects/api.effects.ts index 493fe31634..34ee572143 100644 --- a/src/frontend/packages/store/src/effects/api.effects.ts +++ b/src/frontend/packages/store/src/effects/api.effects.ts @@ -3,13 +3,14 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { mergeMap, withLatestFrom } from 'rxjs/operators'; +import { EntityDeleteCompleteAction } from '../actions/entity.delete.actions'; import { baseRequestPipelineFactory } from '../entity-request-pipeline/base-single-entity-request.pipeline'; import { basePaginatedRequestPipeline } from '../entity-request-pipeline/entity-pagination-request-pipeline'; import { apiRequestPipelineFactory } from '../entity-request-pipeline/entity-request-pipeline'; import { PipelineHttpClient } from '../entity-request-pipeline/pipline-http-client.service'; import { PaginatedAction } from '../types/pagination.types'; -import { ICFAction } from '../types/request.types'; -import { ApiActionTypes } from './../actions/request.actions'; +import { ICFAction, WrapperRequestActionSuccess } from '../types/request.types'; +import { ApiActionTypes, RequestTypes } from './../actions/request.actions'; import { InternalAppState } from './../app-state'; @Injectable() @@ -18,9 +19,7 @@ export class APIEffect { private actions$: Actions, private store: Store, private httpClient: PipelineHttpClient - ) { - - } + ) { } @Effect() apiRequest$ = this.actions$.pipe( @@ -44,4 +43,22 @@ export class APIEffect { }), ); + // Whenever we spot a delete success operation, look to see if the action + // fulfils the entity delete requirements and dispatch an entity delete action if it does + @Effect() + apiDeleteRequest$ = this.actions$.pipe( + ofType(RequestTypes.SUCCESS), + withLatestFrom(this.store), + mergeMap(([action, appState]) => { + if (action.requestType === 'delete') { + const deleteAction = EntityDeleteCompleteAction.parse(action.apiAction); + if (deleteAction) { + // Dispatch a delete action for the entity + this.store.dispatch(deleteAction); + } + } + return []; + }) + ); + } diff --git a/src/frontend/packages/store/src/effects/user-favorites-effect.ts b/src/frontend/packages/store/src/effects/user-favorites-effect.ts index 02240e52af..76e31662f1 100644 --- a/src/frontend/packages/store/src/effects/user-favorites-effect.ts +++ b/src/frontend/packages/store/src/effects/user-favorites-effect.ts @@ -2,8 +2,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; -import { catchError, first, mergeMap, switchMap } from 'rxjs/operators'; +import { catchError, first, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators'; +import { EntityDeleteCompleteAction } from '../actions/entity.delete.actions'; import { ClearPaginationOfEntity } from '../actions/pagination.actions'; import { GetUserFavoritesAction, @@ -17,24 +18,24 @@ import { UpdateUserFavoriteMetadataAction, UpdateUserFavoriteMetadataSuccessAction, } from '../actions/user-favourites.actions'; -import { DispatchOnlyAppState } from '../app-state'; +import { InternalAppState } from '../app-state'; import { entityCatalog } from '../entity-catalog/entity-catalog'; import { proxyAPIVersion } from '../jetstream'; import { NormalizedResponse } from '../types/api.types'; import { StartRequestAction, WrapperRequestActionFailed, WrapperRequestActionSuccess } from '../types/request.types'; import { IFavoriteMetadata, UserFavorite, userFavoritesPaginationKey } from '../types/user-favorites.types'; import { UserFavoriteManager } from '../user-favorite-manager'; +import { STRATOS_ENDPOINT_TYPE, userFavouritesEntityType } from './../helpers/stratos-entity-factory'; const favoriteUrlPath = `/pp/${proxyAPIVersion}/favorites`; - @Injectable() export class UserFavoritesEffect { constructor( private http: HttpClient, private actions$: Actions, - private store: Store, + private store: Store, private userFavoriteManager: UserFavoriteManager ) { } @@ -144,4 +145,20 @@ export class UserFavoritesEffect { ); }) ); + + @Effect() + entityDeleteRequest$ = this.actions$.pipe( + ofType(EntityDeleteCompleteAction.ACTION_TYPE), + withLatestFrom(this.store), + mergeMap(([action, appState]) => { + // If there is a favorite, delete it + const fav = action.asFavorite(); + const entityKey = entityCatalog.getEntityKey(STRATOS_ENDPOINT_TYPE, userFavouritesEntityType); + if (appState.requestData && appState.requestData[entityKey] && appState.requestData[entityKey][fav.guid]) { + this.store.dispatch(new RemoveUserFavoriteAction(fav)); + } + return []; + }) + ); + } diff --git a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts index f1e84bafca..f63f5a5781 100644 --- a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts +++ b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.helpers.ts @@ -1,3 +1,4 @@ +import { EntityDeleteCompleteAction } from '../../actions/entity.delete.actions'; import { AddRecentlyVisitedEntityAction } from '../../actions/recently-visited.actions'; import { IRecentlyVisitedEntity, IRecentlyVisitedState } from '../../types/recently-visited.types'; @@ -62,3 +63,11 @@ export function cleanRecentsList(state: IRecentlyVisitedState, endpointGuids: st // Convert the array back into a map return filtered.reduce(recentArrayToMap, {}); } + +export function clearEntityFromRecentsList(state: IRecentlyVisitedState, action: EntityDeleteCompleteAction): IRecentlyVisitedState { + // Remove entity from the map if it exists + const fav = action.asFavorite(); + const newState = { ...state }; + delete newState[fav.guid]; + return newState; +} \ No newline at end of file diff --git a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts index 79fd5eaee4..19e21f17d9 100644 --- a/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts +++ b/src/frontend/packages/store/src/reducers/current-user-roles-reducer/recently-visited.reducer.ts @@ -7,17 +7,25 @@ import { GetAllEndpointsSuccess, UNREGISTER_ENDPOINTS_SUCCESS, } from '../../actions/endpoint.actions'; +import { EntityDeleteCompleteAction } from '../../actions/entity.delete.actions'; import { AddRecentlyVisitedEntityAction, SetRecentlyVisitedEntityAction } from '../../actions/recently-visited.actions'; import { entityCatalog } from '../../entity-catalog/entity-catalog'; import { endpointEntityType, STRATOS_ENDPOINT_TYPE } from '../../helpers/stratos-entity-factory'; import { IRecentlyVisitedState } from '../../types/recently-visited.types'; -import { addRecentlyVisitedEntity, cleanRecentsList, getDefaultRecentState } from './recently-visited.reducer.helpers'; +import { + addRecentlyVisitedEntity, + cleanRecentsList, + clearEntityFromRecentsList, + getDefaultRecentState, +} from './recently-visited.reducer.helpers'; export function recentlyVisitedReducer( state: IRecentlyVisitedState = getDefaultRecentState(), action: Action ): IRecentlyVisitedState { switch (action.type) { + case EntityDeleteCompleteAction.ACTION_TYPE: + return clearEntityFromRecentsList(state, action as EntityDeleteCompleteAction); case AddRecentlyVisitedEntityAction.ACTION_TYPE: return addRecentlyVisitedEntity(state, action as AddRecentlyVisitedEntityAction); case SetRecentlyVisitedEntityAction.ACTION_TYPE: diff --git a/src/jetstream/datastore/database_cf_config.go b/src/jetstream/datastore/database_cf_config.go index 74509dc65c..614716b633 100644 --- a/src/jetstream/datastore/database_cf_config.go +++ b/src/jetstream/datastore/database_cf_config.go @@ -47,10 +47,10 @@ func ParseCFEnvs(db *DatabaseConfig, env *env.VarSet) (bool, error) { log.Info("No DB configurations defined, will use SQLite") return false, nil } - return findDatabaseConfig(vcapServices, db), nil + return findDatabaseConfig(vcapServices, db, env), nil } -func findDatabaseConfig(vcapServices map[string][]VCAPService, db *DatabaseConfig) bool { +func findDatabaseConfig(vcapServices map[string][]VCAPService, db *DatabaseConfig, env *env.VarSet) bool { var service VCAPService configs := findDatabaseConfigurations(vcapServices) log.Infof("Found %d database service instances", len(configs)) @@ -78,7 +78,7 @@ func findDatabaseConfig(vcapServices map[string][]VCAPService, db *DatabaseConfi db.Username = getDBCredentialsValue(dbCredentials["username"]) db.Password = getDBCredentialsValue(dbCredentials["password"]) db.Host = getDBCredentialsValue(dbCredentials["hostname"]) - db.SSLMode = "disable" + db.SSLMode = env.String("DB_SSL_MODE", "disable") db.Port, _ = strconv.Atoi(getDBCredentialsValue(dbCredentials["port"])) // Note - Both isPostgresService and isMySQLService look at the credentials uri & tags if isPostgresService(service) { diff --git a/website/README.md b/website/README.md index 8354102908..b70a5be1d0 100644 --- a/website/README.md +++ b/website/README.md @@ -7,7 +7,7 @@ This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern ### Installing Dependencies ``` -$ npm +$ npm install ``` ### Local Development diff --git a/website/docs/extensions/frontend.md b/website/docs/extensions/frontend.md index 6bed192aae..a253e906ef 100644 --- a/website/docs/extensions/frontend.md +++ b/website/docs/extensions/frontend.md @@ -3,42 +3,20 @@ title: Frontend Extensions sidebar_label: Frontend Extensions --- -Stratos exposes the following extension points: +An example illustrating the various front-end extension points of Stratos is included in the folder `examples/custom-src`. -- Adding new items to the side navigation menu -- Adding new tabs to the Application, Cloud Foundry, Organization and Space views -- Adding new action buttons to the Application Wall, Application, Cloud Foundry, Organization and Space and Endpoint views -- Replace the loading page -- Replace the login page +To include the customizations in this example, either copy or symlink the `examples/custom-src` to `custom-src` at the top-level of the Stratos repository. -We use Decorators to annotate components to indicate that they are Stratos extensions. +Next, run the customization script (this is done automatically when you do an `npm install`) with: -An example illustrating the various front-end extension points of Stratos is included in the folder `src/frontend/packages/example-extensions`. +``` +npm run customize +``` -To run Stratos with these customizations see [here](/docs/extensions/introduction#acme). +You can now run Stratos locally to see the customizations - see the [Developer's Guide](../developer/frontend) for details. For a walk-through of extending Stratos, see [Example: Adding a Custom Tab](#example-adding-a-custom-tab). -## Including modules and routes -To include code and angular routing in your component there needs to be two access points -- A core module that declares your extension components. -- A core routing module that calls `RouterModule.forRoot`. This is the root for all routes in your package. - -These modules should be made available externally to Stratos by the following steps - -1. Exported in the package's `public-api.ts`, for example `src/frontend/packages/example-extensions/src/public-api.ts`. The public api should be added to the applications `tsconfig.json` for example `src/tsconfig.json` -1. Reference as, or imported by, the two modules defined in the `stratos` section in the package's `package.json`, for example `src/frontend/packages/example-extensions/package.json` - ``` - "stratos": { - ... - "module": "ExampleModule", - "routingModule": "ExampleRoutingModule" - ... - } - ``` - -At build time these are then added to a dynamically created module `src/frontend/packages/core/src/_custom-import.module.ts` and included in the output. - ## Extension Points ### Side Navigation @@ -47,9 +25,9 @@ New items can be added to the Side Navigation menu with extensions. To do so, annotate the routes for your extension with custom metadata, which Stratos will then pick up and add to the side menu. -A full example is in `src/frontend/packages/example-extensions/src/example-routing.module.ts` and `src/frontend/packages/example-extensions/src/nav-extension`. +A full example is in the folder `examples/custom-src/frontend/app/custom/nav-extension`. -Your route should have the following metadata in the `data` field of the route: +Your route should have the following metadata in the `data` field: ``` stratosNavigation: { @@ -60,17 +38,17 @@ Your route should have the following metadata in the `data` field of the route: Where `` is the text label to show in the side navigation and `<ICON NAME>` is the icon to use. -> The routing module must be, or referenced by, the core routing module as described [above](/docs/extensions/frontend#including-modules-and-routes) +You should place your route declaration in a module named `CustomRoutingModule` in the file `custom-src/frontend/app/custom/custom-routing.module.ts`. An example routing module would be: ``` import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { Routes, RouterModule } from '@angular/router'; const customRoutes: Routes = [{ path: 'example', - loadChildren: () => import('./nav-extension/nav-extension.module').then(m => m.NavExtensionModule), + loadChildren: 'app/custom/nav-extension/nav-extension.module#NavExtensionModule', data: { stratosNavigation: { text: 'Example', @@ -85,12 +63,10 @@ const customRoutes: Routes = [{ ], declarations: [] }) -export class ExampleRoutingModule { } - +export class CustomRoutingModule { } ``` -This approach ensures that the Angular compiler creates a separate chunk for the extension at compile time which can be lazy loaded at run -time. +This approach ensures that the Angular compiler creates a separate chunk for the extension at compile time. ### Custom Tabs @@ -101,8 +77,6 @@ Tabs can be added to the following views in Stratos: - The Cloud Foundry Org view that shows detail for a Cloud Foundry organization - The Cloud Foundry Space view that shows detail for a Cloud Foundry space -A step by step guide on how to create a custom tab can be found [below](/docs/extensions/frontend#example-adding-a-custom-tab). - For example: ![Example Application tab extension](../../images/extensions/app-tab-example.png) @@ -110,34 +84,28 @@ For example: The approach for all of these is the same: 1. Create a new component that will provide the tab contents -1. Ensure that your component is included in the `EntryComponent` section of your custom module -1. Decorate the component with the `StratosTab` decorator, for example: - ``` - @StratosTab({ - icon: 'extension', - type: StratosTabType.Application, - label: 'Example App Tab', - link: 'example' - }) - ``` - Where: - - < ICON > is the material design icon name for the icon - - < TYPE > indicates where the tab should appear and can be: - - StratosTabType.Application - Application View - - StratosTabType.CloudFoundry - Cloud Foundry view - - StratosTabType.CloudFoundryOrg - Cloud Foundry Org view - - StratosTabType.CloudFoundrySpace - Cloud Foundry Space view - - < LABEL > is the text label to use for the tab - - < LINK > is the name to use for the route (this must only contain characters permitted in URLs) - An example is included in the file `src/frontend/packages/example-extensions/src/app-tab-extension/app-tab-extension.component.ts`. -1. Declare the component to avoid Angular tree shaking - - In the same module that the component is 'declared' in add the following to `imports` - ``` - ExtensionService.declare([ - <component name>, - ]) - ``` -> The module referencing the component, or another referencing it, must be imported by the core module as described [above](/docs/extensions/frontend#including-modules-and-routes). +2. Ensure that your component is included in the `EntryComponent` section of your custom module +2. Decorate the component with the `StratosTab` decorator, for example: + +``` +@StratosTab({ + type: StratosTabType.Application, + label: '<LABEL>', + link: '<LINK>' +}) +``` + +Where: + +- < TYPE > indicates where the tab should appear and can be: + - StratosTabType.Application - Application View + - StratosTabType.CloudFoundry - Cloud Foundry view + - StratosTabType.CloudFoundryOrg - Cloud Foundry Org view + - StratosTabType.CloudFoundrySpace - Cloud Foundry Space view +- < LABEL > is the text label to use for the tab +- < LINK > is the name to use for the route (this must only contain characters permitted in URLs) + +An example is included in the file `examples/custom-src/frontend/app/custom/app-tab-extension`. ### Custom Actions @@ -157,239 +125,92 @@ An action is a icon button that appears at the top-right of a View. For example: The approach for all of these is the same: 1. Create a new component that will provide the contents to show when the action is clicked -1. Ensure that your component is included in the `EntryComponent` section of your custom module -1. Decorate the component with the `StratosAction` decorator, for example: - ``` - @StratosAction({ - type: StratosActionType.Applications, - label: '<LABEL>', - link: '<LINK>', - icon: '<ICON> - }) - ``` - Where: - - < TYPE > indicates where the action should appear and can be: - - StratosActionType.Applications - Application Wall View - - StratosActionType.Application - Application View - - StratosActionType.CloudFoundry - Cloud Foundry view - - StratosActionType.CloudFoundryOrg - Cloud Foundry Org view - - StratosActionType.CloudFoundrySpace - Cloud Foundry Space view - - StratosActionType.Endpoints - Endpoints view - - < ICON > is the icon to show - - < LABEL > is the text label to use for the tooltip of the icon (optional) - - < LINK > is the name to use for the route (this must only contain characters permitted in URLs) - An example is included in the file `src/frontend/packages/example-extensions/src/app-action-extension/app-action-extension.component.ts`. -1. Declare the component to avoid Angular tree shaking - - In the same module that the component is 'declared' in add the following to `imports`. - ``` - ExtensionService.declare([ - <component name>, - ]) - ``` - -> The module referencing the component, or another referencing it, must be imported by the core module as described in [above](/docs/extensions/frontend#including-modules-and-routes) - -### Loading Indicator - -On slower connections, it can take a few seconds to load the main Javascript resources for Stratos. - -In order to give the user some initial feedback that Stratos is loading, a loading indicator is included in the `index.html` file. This gets shown as early as possible, as soon as this main html file has loaded. Once the main code has been fetched, the view refreshes to show the application. - -A default loading indicator is provided that can be changed. To do so, create the following two in your extension or theme package: - -- `loading.css` - CSS styles to be included in a style block in the head of the index page -- `loading.html` - HTML markup to be included the the index page to render the loading indicator - -Then reference them to your package's `package.json` +2. Ensure that your component is included in the `EntryComponent` section of your custom module +2. Decorate the component with the `StratosAction` decorator, for example: ``` - "stratos": { - ... - "theme": { - "loadingCss": "loader/loading.css", - "loadingHtml": "loader/loading.html" - } - ... - }, +@StratosAction({ + type: StratosActionType.Applications, + label: '<LABEL>', + link: '<LINK>', + icon: '<ICON> +}) ``` -The files for the default indicator can be found in the `src/frontend/packages/theme/loader` folder. - -An example of a different loading indicator is included with the ACME sample in `src/frontend/packages/example-theme/loader`. - -The customization task will insert the appropriate CSS and HTML files into the main index.html file when it runs. - -Take a look at the template for the `index.html` file in `src/frontend/packages/core/misc/custom/index.html`. The CSS file is inserted where the marker `/** @@LOADING_CSS@@ **/` is and the HTML file where `<!-- @@LOADING_HTML@@ -->` is. - -### Login Page - -The log in page can be replaced by another Angular component. This can extend the original log in component and provide the same functionality, -see `src/frontend/packages/example-extensions/src/acme-login`. - -1. Create a new log in component that will contain the same form and fields. The component should have the decorator `@StratosLoginComponent()` - ``` - @StratosLoginComponent() - @Component({ - selector: 'app-acme-login', - templateUrl: './acme-login.component.html', - styleUrls: ['./acme-login.component.scss'], - encapsulation: ViewEncapsulation.None - }) - export class AcmeLoginComponent extends LoginPageComponent { - ... - ``` -1. The new component should be declared and set as an entry point, as well as imported via the extension service, in a module - ``` - imports: [ - ... - ExtensionService.declare([ - AcmeLoginComponent, - ]) - ... - ], - // FIXME: Ensure that anything lazy loaded/in kube endpoint pages is not included here - #3675 - declarations: [ - ... - AcmeLoginComponent, - ... - ], - entryComponents: [ - ... - AcmeLoginComponent, - ... - ] - }) - ``` - -### Other Points - -#### Customization Service -A customization service provides a number of smaller extension points. - -|Property | Description| -|--|--| -|hasEula| True if there's a EULA to show. When set to true the asset `/core/eula.html` must exist. For information about custom package assets see the images section [here](/docs/extensions/theming#new-images). | -|copyright| Text shown at the bottom of the side nav| -|logoText| Text shown with the side nav logo| -|aboutInfoComponent| Replace the component used in the Stratos `About` page| -|supportInfoComponent| Replace the component used to provide support information int he Stratos `About` page| -|noEndpointsComponent| Replace the component used in the Endpoints page when there are no registered endpoints| -|alwaysShowNavForEndpointTypes| True to always show the side nav menu items even if an Endpoint for that type is not connected. For example set to `false` to hide CF based menu items such as `Application` if no CF is connected| - -To utilize these define them in a `CustomizationsMetadata` object and apply them using the Angular service `CustomizationService` +Where: -``` - constructor(cs: CustomizationService) { - const customizations: CustomizationsMetadata = { - copyright: '© 2020 Me', - hasEula: true, - aboutInfoComponent: MyAboutInfoComponent, - noEndpointsComponent: MyWelcomeComponent, - alwaysShowNavForEndpointTypes: (typ) => false, - } - cs.set(customizations); - } +- < TYPE > indicates where the action should appear and can be: + - StratosActionType.Applications - Application Wall View + - StratosActionType.Application - Application View + - StratosActionType.CloudFoundry - Cloud Foundry view + - StratosActionType.CloudFoundryOrg - Cloud Foundry Org view + - StratosActionType.CloudFoundrySpace - Cloud Foundry Space view + - StratosActionType.Endpoints - Endpoints view +- < ICON > is the icon to show +- < LABEL > is the text label to use for the tooltip of the icon (optional) +- < LINK > is the name to use for the route (this must only contain characters permitted in URLs) + +An example is included in the file `examples/custom-src/frontend/app/custom/app-action-extension`. + +## Example: Adding a Custom Tab + +In this example, we will walk through extending the Stratos front-end. + +This walk-through assumes that you have installed the Angular CLI globally - this can be done with `npm install -g @angular/cli`. + +### Create a new module + +First, create the custom-src folder structure - from the top-level of the Stratos repository run: +``` +mkdir -p custom-src/frontend/app/custom +mkdir -p custom-src/frontend/assets/custom ``` -#### stratos.yaml -A few 'higher up' extension points can be found in `./stratos.yaml`. For example the [SUSE stratos.yaml](https://github.com/SUSE/stratos/blob/master/stratos.yaml) is below. +Next, run the customize task: ``` -title: SUSE Stratos Console -productVersion: 2.0.0 +npm run customize ``` -|Property|Description| -|--|--| -|title| Official product title, shown in `About` page and other custom places| -|productVersion| Use when building `helm` charts| +This will symlink our custom folder into the Stratos application source folder. +Next, use the Angular CLI to create the root module for our custom code with: -## Example: Adding a Custom Tab +``` +ng generate module custom +``` -In this example, we will walk through extending the Stratos front-end. A new tab will be added to the Cloud Foundry Application page. +This create a new Angular module `CustomModule`. -This walk-through assumes that you have installed the Angular CLI globally - this can be done with `npm install -g @angular/cli`. +Run the customize script again, now that that we have created the custom module with: -### Create a new extensions package - -> Extension packages can contain many components and even a theme. The example here assumes a fresh start so a new package must be created - -1. Create the directory - ``` - mdkir src/frontend/packages/my-custom-module - ``` -1. Create an angular module in `my-custom-module` called `my-example.module.ts` - ``` - import { CommonModule } from '@angular/common'; - import { NgModule } from '@angular/core'; - - @NgModule({ - imports: [ - CommonModule - ], - declarations: [ - ] - }) - export class MyExampleModule { } - ``` -1. Create a `public-api.ts` in `my-custom-module` and reference it in your applications `tsconfig.json`. - - `public-api.ts` - ``` - export * from './my-example.module'; - ``` - `tsconfig.json` - ``` - "paths": { - ... - "@myexamples/extensions": ["frontend/packages/my-custom-module/public-api.ts"] - .... - } - ``` - -1. Create a `package.json` in `my-custom-module` - ``` - { - "name": "@myexamples/extensions", - "version": "0.0.1", - "peerDependencies": { - "@angular/common": "^6.0.0-rc.0 || ^6.0.0", - "@angular/core": "^6.0.0-rc.0 || ^6.0.0" - }, - "stratos": { - "module": "MyExampleModule" - } - } - ``` +``` +npm run customize +``` ### Create a new Component for our Tab Create a new Angular component with the CLI: ``` -cd src/frontend/packages/my-custom-module -ng generate component example-tab-extension +ng generate component custom/example-tab-extension ``` -This will automatically declare the component in `MyExampleModule` - ### Add Decorator to make this Component an Extension In a text editor, open the file: ``` -src/frontend/packages/my-custom-module/example-tab-extension/example-tab-extension.component.ts +src/frontend/app/custom/example-tab-extension/example-tab-extension.component.ts ``` Add the following decorator to the component at the top of the file: ``` -import { StratosTab, StratosTabType } from '@stratosui/core'; +import { StratosTab, StratosTabType } from '../../core/extension/extension-service'; @StratosTab({ - icon: 'done', type: StratosTabType.Application, label: 'Example App Tab', link: 'example' @@ -400,10 +221,9 @@ The file should now look like this: ``` import { Component, OnInit } from '@angular/core'; -import { StratosTab, StratosTabType } from '@stratosui/core'; +import { StratosTab, StratosTabType } from '../../core/extension/extension-service'; @StratosTab({ - icon: 'done', type: StratosTabType.Application, label: 'Example App Tab', link: 'example' @@ -417,7 +237,7 @@ export class ExampleTabExtensionComponent implements OnInit { constructor() { } - ngOnInit(): void { + ngOnInit() { } } @@ -425,35 +245,27 @@ export class ExampleTabExtensionComponent implements OnInit { Save the file. -### Update the module -The component must now be marked as an entry component and imported in such a way angular tree shaking is avoided. +### Mark the component as an entry component -To do this, in a text editor, open the file `src/frontend/packages/my-custom-module/my-example.module.ts` update -- the file imports section -- the module import array -- the entry component array +The last thing we need to do is to mark our Extension component as an entry component. -``` -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { ExtensionService } from '@stratosui/core'; - -import { ExampleTabExtensionComponent } from './example-tab-extension/example-tab-extension.component'; +To do this, in a text editor, open the file `src/frontend/app/custom/custom.module.ts` and add the entry components section so it looks like this: +``` @NgModule({ imports: [ - CommonModule, - ExtensionService.declare([ - ExampleTabExtensionComponent, - ]) + CommonModule ], declarations: [ExampleTabExtensionComponent], entryComponents: [ExampleTabExtensionComponent] }) -export class MyExampleModule { } +export class CustomModule { } ``` ### Run it -You should now be able to run Stratos [locally](/docs/developer/introduction) and see this new tab on the application page for an application. +You should now be able to run Stratos locally and see this new tab on the application page for an application - as illustrated below: + +![Example tab extension](../../images/extensions/tab-example.png) + diff --git a/website/docs/extensions/v4-migration.md b/website/docs/extensions/v4-migration.md index 1b390bc320..9ec035b6b6 100644 --- a/website/docs/extensions/v4-migration.md +++ b/website/docs/extensions/v4-migration.md @@ -43,4 +43,4 @@ To aid in migrating we've provided these instructions. ## Further Guidance Our ACME demo (`src/frontend/packages/example-extensions` and `src/frontend/packages/example-theme`) and SUSE repo ([theme](https://github.com/SUSE/stratos/tree/master/src/frontend/packages/suse-theme) and [extensions](https://github.com/SUSE/stratos/tree/master/src/frontend/packages/suse-extensions)) have both been updated and are fully compatible with the 4.0 changes. Both are a good source for examples. -If there any questions or issues please reach out to us either on out Github [repo](https://github.com/cloudfoundry/stratos) or Slack room [#stratos](https://cloudfoundry.slack.com/?redir=%2Fmessages%2Fstratos). \ No newline at end of file +If there any questions or issues please reach out to us either on out Github [repo](https://github.com/cloudfoundry/stratos) or Slack room [#stratos](https://cloudfoundry.slack.com/?redir=%2Fmessages%2Fstratos). diff --git a/website/sidebars.js b/website/sidebars.js index e286c79021..6bd9866eb1 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -44,8 +44,9 @@ module.exports = { ], 'Extending Stratos': [ 'extensions/introduction', - 'extensions/v4-migration', 'extensions/theming', + 'extensions/customizing', + 'extensions/v4-migration', 'extensions/frontend', 'extensions/backend', ],