Skip to content

Commit cd8acb0

Browse files
author
Marcin Maciaszczyk
authored
Use WebSocket to get global settings (#2008)
* Add initial implementation * Update env files * Update proxy configs * Remove redundant refresh * Fix observable * Update websocket root * Separate websocket proxy config * Fix formatting * Update proxy configs * Use JS instead of JSON for proxy config * Record e2e * Retry websocket connections * Handle error * Fix formatting * Cleanup CI scripts * Revert local proxy config * Further adjustments * Further adjustments * Further adjustments * Add some comments * Add defaulting * Fix imports * Organize code * Turn off video recording * Log websocket connection errors in the console * Fix comments * Rebuild custom link form on changes * Fix local proxy * Fix proxy * Format files * Overwrite origin header * Log ws errors in debug mode only * Fix notification
1 parent dd8647c commit cd8acb0

17 files changed

+84
-123
lines changed

.prow.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
presubmits:
2-
- name: pull-dashboard-headless
2+
- name: pull-dashboard-test-headless
33
always_run: true
44
decorate: true
55
clone_uri: "ssh://[email protected]/kubermatic/dashboard-v2.git"
@@ -24,7 +24,7 @@ presubmits:
2424
name: kubermatic-codecov
2525
key: token
2626

27-
- name: pull-dashboard-e2e
27+
- name: pull-dashboard-test-e2e
2828
always_run: true
2929
decorate: true
3030
clone_uri: "ssh://[email protected]/kubermatic/dashboard-v2.git"
@@ -48,7 +48,7 @@ presubmits:
4848
- image: quay.io/kubermatic/e2e-kind-cypress:v1.1.1
4949
command:
5050
- make
51-
- run-e2e-ci-v2
51+
- run-e2e-ci
5252
securityContext:
5353
privileged: true
5454
resources:

Makefile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ test-headless: install
3232
./hack/upload-coverage.sh
3333

3434
run-e2e-ci: install
35-
./hack/e2e/run_ci_e2e_test.sh
36-
37-
run-e2e-ci-v2: install
3835
./hack/e2e/ci-e2e.sh
3936

4037
dist: install

angular.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
"builder": "@angular-devkit/build-angular:dev-server",
106106
"options": {
107107
"browserTarget": "kubermatic:build",
108-
"proxyConfig": "./proxy.conf.json",
108+
"proxyConfig": "./proxy.conf.js",
109109
"port": 8000,
110110
"hmrWarning": false
111111
},
@@ -114,7 +114,7 @@
114114
"browserTarget": "kubermatic:build:production"
115115
},
116116
"local": {
117-
"proxyConfig": "./proxy-local.conf.json"
117+
"proxyConfig": "./proxy-local.conf.js"
118118
},
119119
"e2e": {
120120
"hmr": false,
@@ -123,7 +123,7 @@
123123
"e2e-local": {
124124
"hmr": false,
125125
"browserTarget": "kubermatic:build:e2e-local",
126-
"proxyConfig": "./proxy-local.conf.json"
126+
"proxyConfig": "./proxy-local.conf.js"
127127
}
128128
}
129129
},

hack/e2e/run_ci_e2e_test.sh

Lines changed: 0 additions & 57 deletions
This file was deleted.

proxy-local.conf.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const PROXY_CONFIG = [
2+
{
3+
context: [
4+
"/api/**",
5+
],
6+
target: "http://localhost:8080",
7+
changeOrigin: true,
8+
secure: false,
9+
ws: true,
10+
}
11+
];
12+
13+
module.exports = PROXY_CONFIG;

proxy-local.conf.json

Lines changed: 0 additions & 7 deletions
This file was deleted.

proxy.conf.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const PROXY_CONFIG = [
2+
{
3+
context: [
4+
"/api/**",
5+
],
6+
target: "https://dev.kubermatic.io",
7+
changeOrigin: true,
8+
headers: {
9+
'Origin': 'https://dev.kubermatic.io',
10+
},
11+
secure: false,
12+
ws: true,
13+
}
14+
];
15+
16+
module.exports = PROXY_CONFIG;

proxy.conf.json

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/app/cluster/cluster-details/cluster-details.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
target="_blank"
4343
mat-flat-button
4444
[disabled]="!isClusterRunning || (!isEditEnabled() && isOpenshiftCluster())"
45-
*ngIf="(settings.adminSettings | async).enableDashboard">
45+
*ngIf="(settings.adminSettings | async)?.enableDashboard">
4646
{{getConnectName()}}
4747
</a>
4848
</div>

src/app/core/services/settings/settings.service.ts

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {HttpClient} from '@angular/common/http';
22
import {Injectable} from '@angular/core';
3-
import {iif, merge, Observable, of, Subject, timer} from 'rxjs';
4-
import {catchError, map, shareReplay, switchMap} from 'rxjs/operators';
3+
import {BehaviorSubject, iif, merge, Observable, of, Subject, timer} from 'rxjs';
4+
import {catchError, delay, map, retryWhen, shareReplay, switchMap, tap} from 'rxjs/operators';
5+
import {webSocket} from 'rxjs/webSocket';
56

67
import {Auth} from '..';
78
import {environment} from '../../../../environments/environment';
@@ -31,15 +32,19 @@ const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
3132
enableOIDCKubeconfig: false,
3233
};
3334

34-
@Injectable()
35+
@Injectable({
36+
providedIn: 'root',
37+
})
3538
export class SettingsService {
36-
private readonly restRoot: string = environment.restRoot;
39+
private readonly restRoot = environment.restRoot;
40+
private readonly wsProtocol = window.location.protocol.replace('http', 'ws');
41+
private readonly wsRoot = `${this.wsProtocol}//${window.location.host}/${this.restRoot}/ws`;
3742
private _userSettings$: Observable<UserSettings>;
38-
private _userSettingsRefresh$: Subject<any> = new Subject();
39-
private _adminSettings$: Observable<AdminSettings>;
40-
private _adminSettingsRefresh$: Subject<any> = new Subject();
43+
private _userSettingsRefresh$ = new Subject();
44+
private readonly _adminSettings$ = new BehaviorSubject(DEFAULT_ADMIN_SETTINGS);
45+
private _adminSettingsWatch$: Observable<AdminSettings>;
4146
private _admins$: Observable<AdminEntity[]>;
42-
private _adminsRefresh$: Subject<any> = new Subject();
47+
private _adminsRefresh$ = new Subject();
4348
private _refreshTimer$ = timer(0, this._appConfigService.getRefreshTimeBase() * 5);
4449

4550
constructor(
@@ -86,28 +91,29 @@ export class SettingsService {
8691
}
8792

8893
get adminSettings(): Observable<AdminSettings> {
89-
if (!this._adminSettings$) {
90-
this._adminSettings$ =
91-
merge(this._refreshTimer$, this._adminSettingsRefresh$)
92-
.pipe(switchMap(
93-
() =>
94-
iif(() => this._auth.authenticated(), this._getAdminSettings(true), of(DEFAULT_ADMIN_SETTINGS))))
95-
.pipe(map(settings => this._defaultAdminSettings(settings)))
96-
.pipe(shareReplay({refCount: true, bufferSize: 1}));
94+
// Subscribe to websocket and proxy all the settings updates coming from the API to the subject that is
95+
// exposed in this method. Thanks to that it is possible to have default value and retry mechanism that
96+
// will run in the background if connection will fail. Subscription to the API should happen only once.
97+
// Behavior subject is used internally to always emit last value when subscription happens.
98+
if (!this._adminSettingsWatch$) {
99+
const webSocket$ =
100+
webSocket<AdminSettings>(`${this.wsRoot}/admin/settings`)
101+
.asObservable()
102+
.pipe(retryWhen(
103+
// Display error in the console for debugging purposes, otherwise it would be ignored.
104+
// tslint:disable-next-line:no-console
105+
errors => errors.pipe(tap(console.debug), delay(this._appConfigService.getRefreshTimeBase() * 3))));
106+
this._adminSettingsWatch$ = iif(() => this._auth.authenticated(), webSocket$, of(DEFAULT_ADMIN_SETTINGS));
107+
this._adminSettingsWatch$.subscribe(settings => this._adminSettings$.next(this._defaultAdminSettings(settings)));
97108
}
109+
98110
return this._adminSettings$;
99111
}
100112

101113
get defaultAdminSettings(): AdminSettings {
102114
return DEFAULT_ADMIN_SETTINGS;
103115
}
104116

105-
private _getAdminSettings(defaultOnError = false): Observable<AdminSettings> {
106-
const url = `${this.restRoot}/admin/settings`;
107-
const observable = this._httpClient.get<AdminSettings>(url);
108-
return defaultOnError ? observable.pipe(catchError(() => of(DEFAULT_ADMIN_SETTINGS))) : observable;
109-
}
110-
111117
private _defaultAdminSettings(settings: AdminSettings): AdminSettings {
112118
if (!settings) {
113119
return DEFAULT_ADMIN_SETTINGS;
@@ -120,10 +126,6 @@ export class SettingsService {
120126
return settings;
121127
}
122128

123-
refreshAdminSettings(): void {
124-
this._adminSettingsRefresh$.next();
125-
}
126-
127129
patchAdminSettings(patch: any): Observable<AdminSettings> {
128130
const url = `${this.restRoot}/admin/settings`;
129131
return this._httpClient.patch<AdminSettings>(url, patch);

src/app/settings/admin/admin-settings.component.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,18 @@ export class AdminSettingsComponent implements OnInit, OnChanges, OnDestroy {
5858

5959
this._settingsService.adminSettings.pipe(takeUntil(this._unsubscribe)).subscribe(settings => {
6060
if (!_.isEqual(settings, this.apiSettings)) {
61-
if (this.apiSettings) {
61+
if (this._shouldDisplayUpdateNotification()) {
6262
this._notificationService.success('Successfully applied external settings update');
6363
}
6464
this._applySettings(settings);
6565
}
6666
});
6767

68-
this._settingsChange.pipe(debounceTime(1000))
68+
this._settingsChange.pipe(debounceTime(500))
6969
.pipe(takeUntil(this._unsubscribe))
7070
.pipe(switchMap(() => this._settingsService.patchAdminSettings(this._getPatch())))
7171
.subscribe(settings => {
7272
this._applySettings(settings);
73-
this._settingsService.refreshAdminSettings();
7473
});
7574

7675
this._settingsService.userSettings.pipe(takeUntil(this._unsubscribe)).subscribe(settings => {
@@ -88,6 +87,10 @@ export class AdminSettingsComponent implements OnInit, OnChanges, OnDestroy {
8887
this._unsubscribe.complete();
8988
}
9089

90+
private _shouldDisplayUpdateNotification(): boolean {
91+
return this.apiSettings && this.apiSettings !== this._settingsService.defaultAdminSettings;
92+
}
93+
9194
private _applySettings(settings: AdminSettings): void {
9295
this.apiSettings = settings;
9396
this.settings = _.cloneDeep(this.apiSettings);

src/app/settings/admin/custom-link-form/custom-links-form.component.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
1+
import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core';
22
import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
33
import * as _ from 'lodash';
44

@@ -9,7 +9,7 @@ import {CustomLink, CustomLinkLocation} from '../../../shared/utils/custom-link-
99
templateUrl: './custom-links-form.component.html',
1010
styleUrls: ['./custom-links-form.component.scss'],
1111
})
12-
export class CustomLinksFormComponent implements OnInit {
12+
export class CustomLinksFormComponent implements OnInit, OnChanges {
1313
@Input() customLinks: CustomLink[] = [];
1414
@Output() customLinksChange = new EventEmitter<CustomLink[]>();
1515
@Input() apiCustomLinks: CustomLink[] = [];
@@ -22,6 +22,16 @@ export class CustomLinksFormComponent implements OnInit {
2222
}
2323

2424
ngOnInit(): void {
25+
this._buildForm();
26+
}
27+
28+
ngOnChanges(changes: SimpleChanges): void {
29+
if (changes.customLinks.currentValue !== changes.customLinks.previousValue) {
30+
this._buildForm();
31+
}
32+
}
33+
34+
private _buildForm(): void {
2535
this.form = this._formBuilder.group({customLinks: this._formBuilder.array([])});
2636
this.customLinks.forEach(
2737
customLink => this._addCustomLink(customLink.label, customLink.url, customLink.icon, customLink.location));

src/app/wizard/wizard.component.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,8 @@ export class WizardComponent implements OnInit, OnDestroy {
6161
this._settingsService.adminSettings.pipe(first()).subscribe(
6262
settings => this.addNodeData.count = settings.defaultNodeCount);
6363

64-
this._settingsService.adminSettings.pipe(takeUntil(this._unsubscribe)).subscribe(settings => {
65-
this.settings = settings;
66-
});
64+
this._settingsService.adminSettings.pipe(takeUntil(this._unsubscribe))
65+
.subscribe(settings => this.settings = settings);
6766

6867
this.updateSteps();
6968
this._projectService.selectedProject.pipe(takeUntil(this._unsubscribe))

src/environments/environment.e2e.local.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ export const environment = {
66
customCSS: '../../assets/custom/style.css',
77
refreshTimeBase: 1000, // Unit: ms
88
restRoot: 'api/v1',
9-
restRootV3: 'api/v3',
10-
digitalOceanRestRoot: 'https://api.digitalocean.com/v2',
119
oidcProviderUrl: 'http://dex.oauth:5556/dex/auth',
1210
oidcConnectorId: 'local',
1311
animations: false,

src/environments/environment.e2e.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ export const environment = {
66
customCSS: '../../assets/custom/style.css',
77
refreshTimeBase: 1000, // Unit: ms
88
restRoot: 'api/v1',
9-
restRootV3: 'api/v3',
10-
digitalOceanRestRoot: 'https://api.digitalocean.com/v2',
119
oidcProviderUrl: 'https://dev.kubermatic.io/dex/auth',
1210
oidcConnectorId: 'local',
1311
animations: false,

src/environments/environment.prod.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ export const environment = {
66
customCSS: '../assets/custom/style.css',
77
refreshTimeBase: 1000, // Unit: ms
88
restRoot: '/api/v1',
9-
restRootV3: '/api/v3',
10-
digitalOceanRestRoot: 'https://api.digitalocean.com/v2',
119
oidcProviderUrl: window.location.protocol + '//' + window.location.host + '/dex/auth',
1210
oidcConnectorId: null,
1311
animations: true,

src/environments/environment.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ export const environment = {
1111
customCSS: '../../assets/custom/style.css',
1212
refreshTimeBase: 1000, // Unit: ms
1313
restRoot: 'api/v1',
14-
restRootV3: 'api/v3',
15-
digitalOceanRestRoot: 'https://api.digitalocean.com/v2',
1614
oidcProviderUrl: 'https://dev.kubermatic.io/dex/auth',
1715
oidcConnectorId: null,
1816
animations: true,

0 commit comments

Comments
 (0)