diff --git a/modules/ui/src/app/app-routing.module.ts b/modules/ui/src/app/app-routing.module.ts
index fd77640cf..a94a90222 100644
--- a/modules/ui/src/app/app-routing.module.ts
+++ b/modules/ui/src/app/app-routing.module.ts
@@ -19,8 +19,14 @@ import { DevicesComponent } from './pages/devices/devices.component';
import { CanDeactivateGuard } from './guards/can-deactivate.guard';
import { TestrunComponent } from './pages/testrun/testrun.component';
import { RiskAssessmentComponent } from './pages/risk-assessment/risk-assessment.component';
+import { SettingsComponent } from './pages/settings/settings.component';
export const routes: Routes = [
+ {
+ path: 'settings',
+ component: SettingsComponent,
+ title: 'Testrun - Settings',
+ },
{
path: 'testing',
component: TestrunComponent,
diff --git a/modules/ui/src/app/app.component.html b/modules/ui/src/app/app.component.html
index cff15804e..2aeeb52c2 100644
--- a/modules/ui/src/app/app.component.html
+++ b/modules/ui/src/app/app.component.html
@@ -98,9 +98,12 @@
Testrun
mat-icon-button
aria-label="System settings"
matTooltip="Settings"
- (click)="
- openGeneralSettings(true, isTestrunInProgress(vm.systemStatus))
- ">
+ routerLink="{{ Routes.Settings }}"
+ routerLinkActive="app-sidebar-button-active"
+ (keydown.enter)="onNavigationClick()">
+
+
+
tune
@@ -275,11 +278,11 @@ Testrun
position="end"
autoFocus="#setting-panel-close-button"
class="settings-drawer">
-
-
+
{
FakeTestingCompleteComponent,
RouterTestingModule.withRoutes([
{ path: 'devices', children: [] },
+ { path: 'settings', children: [] },
{ path: 'testing', children: [] },
{ path: 'reports', children: [] },
]),
@@ -190,7 +191,7 @@ describe('AppComponent', () => {
}).overrideComponent(AppComponent, {
remove: {
imports: [
- SettingsComponent,
+ GeneralSettingsComponent,
SpinnerComponent,
ShutdownAppComponent,
TestingCompleteComponent,
@@ -368,15 +369,16 @@ describe('AppComponent', () => {
expect(settings.getSystemConfig).toHaveBeenCalled();
});
- it('should call settingsDrawer open on openSetting', fakeAsync(() => {
+ it('should navigate to the settings when "settings" button is clicked', fakeAsync(() => {
fixture.detectChanges();
- const settingsDrawer = component.settingsDrawer();
- spyOn(settingsDrawer, 'open');
- component.openSetting(false);
+ const settingsButton = compiled.querySelector(
+ '.app-toolbar-button-general-settings'
+ ) as HTMLButtonElement;
+ settingsButton?.click();
tick();
- expect(settingsDrawer.open).toHaveBeenCalledTimes(1);
+ expect(router.url).toBe(Routes.Settings);
}));
it('should announce settingsDrawer disabled on openSetting and settings are disabled', fakeAsync(() => {
@@ -394,20 +396,6 @@ describe('AppComponent', () => {
);
}));
- it('should call settingsDrawer open on click settings button', () => {
- fixture.detectChanges();
-
- const settingsBtn = compiled.querySelector(
- '.app-toolbar-button-general-settings'
- ) as HTMLButtonElement;
- const settingsDrawer = component.settingsDrawer();
- spyOn(settingsDrawer, 'open');
-
- settingsBtn.click();
-
- expect(settingsDrawer.open).toHaveBeenCalledTimes(1);
- });
-
it('should have spinner', () => {
const spinner = compiled.querySelector('app-spinner');
@@ -838,7 +826,7 @@ describe('AppComponent', () => {
});
@Component({
- selector: 'app-settings',
+ selector: 'app-general-settings',
template: '',
})
class FakeGeneralSettingsComponent {
diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts
index 7ef9113ac..fc5f7d30e 100644
--- a/modules/ui/src/app/app.component.ts
+++ b/modules/ui/src/app/app.component.ts
@@ -32,7 +32,7 @@ import { FocusManagerService } from './services/focus-manager.service';
import { State, Store } from '@ngrx/store';
import { AppState } from './store/state';
import { setIsOpenAddDevice } from './store/actions';
-import { SettingsComponent } from './pages/settings/settings.component';
+import { GeneralSettingsComponent } from './pages/general-settings/general-settings.component';
import { AppStore } from './app.store';
import { TestRunService } from './services/test-run.service';
import { CdkTrapFocus, LiveAnnouncer } from '@angular/cdk/a11y';
@@ -92,7 +92,7 @@ const QUALIFICATION_URL = '/assets/icons/qualification.svg';
CertificatesComponent,
WifiComponent,
TestingCompleteComponent,
- SettingsComponent,
+ GeneralSettingsComponent,
RouterModule,
CommonModule,
],
@@ -118,7 +118,7 @@ export class AppComponent implements AfterViewInit {
readonly certDrawer = viewChild.required('certDrawer');
readonly toggleSettingsBtn =
viewChild.required('toggleSettingsBtn');
- readonly settings = viewChild.required('settings');
+ readonly settings = viewChild.required('settings');
viewModel$ = this.appStore.viewModel$;
readonly riskAssessmentLink = viewChild('riskAssessmentLink');
diff --git a/modules/ui/src/app/model/routes.ts b/modules/ui/src/app/model/routes.ts
index 05ff91ed9..b2873b126 100644
--- a/modules/ui/src/app/model/routes.ts
+++ b/modules/ui/src/app/model/routes.ts
@@ -16,6 +16,7 @@
export enum Routes {
Devices = '/devices',
+ Settings = '/settings',
Testing = '/testing',
Reports = '/reports',
RiskAssessment = '/risk-assessment',
diff --git a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.html b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.html
similarity index 100%
rename from modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.html
rename to modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.html
diff --git a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.scss b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.scss
similarity index 100%
rename from modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.scss
rename to modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.scss
diff --git a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.spec.ts b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.spec.ts
similarity index 100%
rename from modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.spec.ts
rename to modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.spec.ts
diff --git a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.ts b/modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.ts
similarity index 100%
rename from modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.ts
rename to modules/ui/src/app/pages/general-settings/components/settings-dropdown/settings-dropdown.component.ts
diff --git a/modules/ui/src/app/pages/general-settings/general-settings.component.html b/modules/ui/src/app/pages/general-settings/general-settings.component.html
new file mode 100644
index 000000000..2dd8567b3
--- /dev/null
+++ b/modules/ui/src/app/pages/general-settings/general-settings.component.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+ Warning! No ports detected.
+
+
+
+
diff --git a/modules/ui/src/app/pages/general-settings/general-settings.component.scss b/modules/ui/src/app/pages/general-settings/general-settings.component.scss
new file mode 100644
index 000000000..ea51621ed
--- /dev/null
+++ b/modules/ui/src/app/pages/general-settings/general-settings.component.scss
@@ -0,0 +1,152 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@use '@angular/material' as mat;
+@use 'colors';
+@use 'variables';
+
+:host {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ flex: 1 0 auto;
+}
+
+.settings-drawer-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 12px 16px 24px;
+
+ &-title {
+ margin: 0;
+ font-size: 22px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 28px;
+ color: colors.$dark-grey;
+ }
+
+ &-button {
+ min-width: 24px;
+ width: 24px;
+ height: 24px;
+ margin: 4px;
+ padding: 8px;
+ box-sizing: content-box;
+ line-height: normal !important;
+
+ .close-button-icon {
+ width: 24px;
+ height: 24px;
+ margin: 0;
+ }
+
+ ::ng-deep * {
+ line-height: inherit !important;
+ }
+ }
+}
+
+.setting-drawer-content {
+ padding: 0 16px 8px 16px;
+ overflow: hidden;
+ flex: 1;
+
+ form {
+ display: grid;
+ height: 100%;
+ }
+
+ .setting-drawer-content-form-empty {
+ grid-template-rows: repeat(2, auto) 1fr;
+ }
+}
+
+.setting-drawer-content-inputs {
+ overflow: auto;
+ margin: 0 -16px;
+ padding: 0 16px;
+}
+
+.error-message-container {
+ display: block;
+ margin-top: auto;
+ padding-bottom: 8px;
+}
+
+.error-message-container + .setting-drawer-footer {
+ margin-top: 0;
+}
+
+.message {
+ margin: 0;
+ padding: 6px 0 12px 0;
+ color: colors.$grey-800;
+ font-family: variables.$font-secondary;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.2px;
+}
+
+.setting-drawer-footer {
+ padding: 0 8px;
+ margin-top: auto;
+ display: flex;
+ flex-shrink: 0;
+ justify-content: flex-end;
+
+ .close-button,
+ .save-button {
+ padding: 0 24px;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ letter-spacing: 0.25px;
+ }
+
+ .close-button {
+ margin-right: 10px;
+ }
+}
+
+.settings-disabled-overlay {
+ position: absolute;
+ width: 100%;
+ left: 0;
+ right: 0;
+ top: 75px;
+ bottom: 45px;
+ background-color: rgba(255, 255, 255, 0.7);
+ z-index: 2;
+}
+
+.disabled {
+ .message-link {
+ cursor: default;
+ pointer-events: none;
+
+ &:focus-visible {
+ outline: none;
+ }
+ }
+}
+
+.settings-drawer-header-button:not(.mat-mdc-button-disabled),
+.close-button:not(.mat-mdc-button-disabled),
+.save-button:not(.mat-mdc-button-disabled) {
+ cursor: pointer;
+ pointer-events: auto;
+}
diff --git a/modules/ui/src/app/pages/general-settings/general-settings.component.spec.ts b/modules/ui/src/app/pages/general-settings/general-settings.component.spec.ts
new file mode 100644
index 000000000..a392de2e2
--- /dev/null
+++ b/modules/ui/src/app/pages/general-settings/general-settings.component.spec.ts
@@ -0,0 +1,383 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GeneralSettingsComponent } from './general-settings.component';
+import { of } from 'rxjs';
+import { MatRadioModule } from '@angular/material/radio';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIcon, MatIconModule } from '@angular/material/icon';
+import { MatIconTestingModule } from '@angular/material/icon/testing';
+import { Component, Input } from '@angular/core';
+import { LiveAnnouncer } from '@angular/cdk/a11y';
+import SpyObj = jasmine.SpyObj;
+import { MatInputModule } from '@angular/material/input';
+import { MatSelectModule } from '@angular/material/select';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { provideMockStore } from '@ngrx/store/testing';
+import { LoaderService } from '../../services/loader.service';
+import { GeneralSettingsStore } from './general-settings.store';
+import {
+ MOCK_INTERFACES,
+ MOCK_SYSTEM_CONFIG_WITH_DATA,
+} from '../../mocks/settings.mock';
+import { SettingsDropdownComponent } from './components/settings-dropdown/settings-dropdown.component';
+import { CalloutComponent } from '../../components/callout/callout.component';
+import { SpinnerComponent } from '../../components/spinner/spinner.component';
+
+describe('GeneralSettingsComponent', () => {
+ let component: GeneralSettingsComponent;
+ let fixture: ComponentFixture;
+ let mockLiveAnnouncer: SpyObj;
+ let compiled: HTMLElement;
+ let mockLoaderService: SpyObj;
+ let mockSettingsStore: SpyObj;
+
+ beforeEach(async () => {
+ mockLiveAnnouncer = jasmine.createSpyObj(['announce']);
+ mockLoaderService = jasmine.createSpyObj('LoaderService', ['setLoading']);
+ mockSettingsStore = jasmine.createSpyObj('SettingsStore', [
+ 'getInterfaces',
+ 'updateSystemConfig',
+ 'setIsSubmitting',
+ 'setDefaultFormValues',
+ 'getSystemConfig',
+ 'viewModel$',
+ ]);
+
+ await TestBed.configureTestingModule({
+ providers: [
+ { provide: LiveAnnouncer, useValue: mockLiveAnnouncer },
+ { provide: LoaderService, useValue: mockLoaderService },
+ { provide: GeneralSettingsStore, useValue: mockSettingsStore },
+ provideMockStore(),
+ ],
+ imports: [
+ GeneralSettingsComponent,
+ BrowserAnimationsModule,
+ MatButtonModule,
+ MatIconModule,
+ MatRadioModule,
+ ReactiveFormsModule,
+ MatIconTestingModule,
+ MatIcon,
+ MatInputModule,
+ MatSelectModule,
+ SettingsDropdownComponent,
+ FakeSpinnerComponent,
+ FakeCalloutComponent,
+ ],
+ })
+ .overrideComponent(GeneralSettingsComponent, {
+ remove: {
+ imports: [CalloutComponent, SpinnerComponent],
+ },
+ add: {
+ imports: [FakeSpinnerComponent, FakeCalloutComponent],
+ },
+ })
+ .compileComponents();
+
+ TestBed.overrideProvider(GeneralSettingsStore, {
+ useValue: mockSettingsStore,
+ });
+
+ fixture = TestBed.createComponent(GeneralSettingsComponent);
+
+ component = fixture.componentInstance;
+ component.viewModel$ = of({
+ systemConfig: { network: {} },
+ hasConnectionSettings: false,
+ isSubmitting: false,
+ isLessThanOneInterface: false,
+ interfaces: {},
+ deviceOptions: {},
+ internetOptions: {},
+ logLevelOptions: {},
+ monitoringPeriodOptions: {},
+ });
+ fixture.detectChanges();
+ compiled = fixture.nativeElement as HTMLElement;
+
+ component.ngOnInit();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('#reloadSetting should call setLoading in loaderService', () => {
+ component.reloadSetting();
+
+ expect(mockLoaderService.setLoading).toHaveBeenCalledWith(true);
+ });
+
+ describe('#settingsDisable', () => {
+ it('should disable setting form when get settingDisable as true ', () => {
+ spyOn(component.settingForm, 'disable');
+
+ component.settingsDisable = true;
+
+ expect(component.settingForm.disable).toHaveBeenCalled();
+ });
+
+ it('should enable setting form when get settingDisable as false ', () => {
+ spyOn(component.settingForm, 'enable');
+
+ component.settingsDisable = false;
+
+ expect(component.settingForm.enable).toHaveBeenCalled();
+ });
+
+ it('should disable "Save" button when get settingDisable as true', () => {
+ component.settingsDisable = true;
+
+ const saveBtn = compiled.querySelector(
+ '.save-button'
+ ) as HTMLButtonElement;
+
+ expect(saveBtn.disabled).toBeTrue();
+ });
+
+ it('should disable "Refresh" link when settingDisable', () => {
+ component.settingsDisable = true;
+
+ const refreshLink = compiled.querySelector(
+ '.message-link'
+ ) as HTMLAnchorElement;
+
+ refreshLink.click();
+
+ expect(refreshLink.hasAttribute('aria-disabled')).toBeTrue();
+ expect(mockLoaderService.setLoading).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('#closeSetting', () => {
+ beforeEach(() => {
+ component.ngOnInit();
+ });
+
+ it('should emit closeSettingEvent', () => {
+ spyOn(component.closeSettingEvent, 'emit');
+
+ component.closeSetting('Message');
+
+ expect(component.closeSettingEvent.emit).toHaveBeenCalled();
+ });
+
+ it('should call liveAnnouncer with provided message', () => {
+ const mockMessage = 'mock event';
+
+ component.closeSetting(mockMessage);
+
+ expect(mockLiveAnnouncer.announce).toHaveBeenCalledWith(
+ `The ${mockMessage} finished. The system settings panel is closed.`
+ );
+ });
+
+ it('should call reset settingForm', () => {
+ spyOn(component.settingForm, 'reset');
+
+ component.closeSetting('Message');
+
+ expect(component.settingForm.reset).toHaveBeenCalled();
+ });
+
+ it('should call setDefaultFormValues', () => {
+ component.closeSetting('Message');
+
+ expect(mockSettingsStore.setDefaultFormValues).toHaveBeenCalled();
+ });
+ });
+
+ describe('#saveSetting', () => {
+ beforeEach(() => {
+ component.ngOnInit();
+ });
+
+ it('should have form error if form has the same value', () => {
+ const mockSameValue = 'sameValue';
+ component.deviceControl.setValue(mockSameValue);
+ component.internetControl.setValue(mockSameValue);
+
+ component.saveSetting();
+
+ expect(component.settingForm.invalid).toBeTrue();
+ expect(component.isFormError).toBeTrue();
+ expect(mockSettingsStore.setIsSubmitting).toHaveBeenCalledWith(true);
+ });
+
+ it('should call createSystemConfig when setting form valid', () => {
+ const expectedResult = {
+ network: {
+ device_intf: 'mockDeviceKey',
+ internet_intf: '',
+ },
+ log_level: 'INFO',
+ monitor_period: 600,
+ };
+
+ component.deviceControl.setValue({
+ key: 'mockDeviceKey',
+ value: 'mockDeviceValue',
+ });
+
+ component.internetControl.setValue({
+ key: '',
+ value: 'defaultValue',
+ });
+
+ component.logLevel.setValue({
+ key: 'INFO',
+ value: '',
+ });
+
+ component.monitorPeriod.setValue({
+ key: '600',
+ value: '',
+ });
+
+ component.saveSetting();
+
+ const args = mockSettingsStore.updateSystemConfig.calls.argsFor(0);
+ // @ts-expect-error config is in object
+ expect(args[0].config).toEqual(expectedResult);
+ expect(component.settingForm.invalid).toBeFalse();
+ expect(mockSettingsStore.updateSystemConfig).toHaveBeenCalled();
+ });
+ });
+
+ describe('with no interfaces data', () => {
+ beforeEach(() => {
+ component.viewModel$ = of({
+ systemConfig: { network: {} },
+ hasConnectionSettings: false,
+ isSubmitting: false,
+ isLessThanOneInterface: false,
+ interfaces: {},
+ deviceOptions: {},
+ internetOptions: {},
+ logLevelOptions: {},
+ monitoringPeriodOptions: {},
+ });
+ fixture.detectChanges();
+ });
+
+ it('should have callout component', () => {
+ const callout = compiled.querySelector('app-callout');
+
+ expect(callout).toBeTruthy();
+ });
+
+ it('should have disabled "Save" button', () => {
+ const saveBtn = compiled.querySelector(
+ '.save-button'
+ ) as HTMLButtonElement;
+
+ expect(saveBtn.disabled).toBeTrue();
+ });
+ });
+
+ describe('with interfaces length less than one', () => {
+ beforeEach(() => {
+ component.viewModel$ = of({
+ systemConfig: { network: {} },
+ hasConnectionSettings: false,
+ isSubmitting: false,
+ isLessThanOneInterface: true,
+ interfaces: {},
+ deviceOptions: {},
+ internetOptions: {},
+ logLevelOptions: {},
+ monitoringPeriodOptions: {},
+ });
+ fixture.detectChanges();
+ });
+
+ it('should have disabled "Save" button', () => {
+ component.deviceControl.setValue(
+ MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf
+ );
+ component.internetControl.setValue(
+ MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.internet_intf
+ );
+ fixture.detectChanges();
+
+ const saveBtn = compiled.querySelector(
+ '.save-button'
+ ) as HTMLButtonElement;
+
+ expect(saveBtn.disabled).toBeTrue();
+ });
+ });
+
+ describe('with interfaces length more then one', () => {
+ beforeEach(() => {
+ component.viewModel$ = of({
+ systemConfig: { network: {} },
+ hasConnectionSettings: false,
+ isSubmitting: false,
+ isLessThanOneInterface: false,
+ interfaces: MOCK_INTERFACES,
+ deviceOptions: MOCK_INTERFACES,
+ internetOptions: MOCK_INTERFACES,
+ logLevelOptions: {},
+ monitoringPeriodOptions: {},
+ });
+ fixture.detectChanges();
+ });
+
+ it('should not have callout component', () => {
+ const callout = compiled.querySelector('app-callout');
+
+ expect(callout).toBeFalsy();
+ });
+
+ it('should not have disabled "Save" button', () => {
+ component.deviceControl.setValue({
+ key: MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf,
+ value: 'value',
+ });
+ component.internetControl.setValue({
+ key: MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.internet_intf,
+ value: 'value',
+ });
+ fixture.detectChanges();
+
+ const saveBtn = compiled.querySelector(
+ '.save-button'
+ ) as HTMLButtonElement;
+
+ expect(saveBtn.disabled).toBeFalse();
+ });
+ });
+});
+
+@Component({
+ selector: 'app-spinner',
+ template: '',
+})
+class FakeSpinnerComponent {}
+
+@Component({
+ selector: 'app-callout',
+ template: '',
+})
+class FakeCalloutComponent {
+ @Input() type = '';
+}
diff --git a/modules/ui/src/app/pages/general-settings/general-settings.component.ts b/modules/ui/src/app/pages/general-settings/general-settings.component.ts
new file mode 100644
index 000000000..a4bae3b20
--- /dev/null
+++ b/modules/ui/src/app/pages/general-settings/general-settings.component.ts
@@ -0,0 +1,267 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ ChangeDetectionStrategy,
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnDestroy,
+ OnInit,
+ Output,
+ viewChild,
+ inject,
+} from '@angular/core';
+import {
+ FormBuilder,
+ FormControl,
+ FormGroup,
+ ReactiveFormsModule,
+} from '@angular/forms';
+import { Subject, takeUntil, tap } from 'rxjs';
+import { OnlyDifferentValuesValidator } from './only-different-values.validator';
+import { CalloutType } from '../../model/callout-type';
+import { CdkTrapFocus, LiveAnnouncer } from '@angular/cdk/a11y';
+import { EventType } from '../../model/event-type';
+import { FormKey, SystemConfig } from '../../model/setting';
+import { GeneralSettingsStore } from './general-settings.store';
+import { LoaderService } from '../../services/loader.service';
+import { MatToolbarModule } from '@angular/material/toolbar';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatButtonToggleModule } from '@angular/material/button-toggle';
+import { MatInputModule } from '@angular/material/input';
+import { MatSnackBarModule } from '@angular/material/snack-bar';
+import { SpinnerComponent } from '../../components/spinner/spinner.component';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { MatButtonModule } from '@angular/material/button';
+import { SettingsDropdownComponent } from './components/settings-dropdown/settings-dropdown.component';
+import { MatSelectModule } from '@angular/material/select';
+import { MatIconModule } from '@angular/material/icon';
+import { MatSidenavModule } from '@angular/material/sidenav';
+import { MatRadioModule } from '@angular/material/radio';
+import { CalloutComponent } from '../../components/callout/callout.component';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'app-general-settings',
+ templateUrl: './general-settings.component.html',
+ styleUrls: ['./general-settings.component.scss'],
+ hostDirectives: [CdkTrapFocus],
+ imports: [
+ MatButtonModule,
+ MatIconModule,
+ MatToolbarModule,
+ MatSidenavModule,
+ MatButtonToggleModule,
+ MatRadioModule,
+ MatInputModule,
+ MatSelectModule,
+ MatTooltipModule,
+ ReactiveFormsModule,
+ MatFormFieldModule,
+ MatSnackBarModule,
+ SpinnerComponent,
+ CalloutComponent,
+ SettingsDropdownComponent,
+ CommonModule,
+ ],
+ providers: [GeneralSettingsStore],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class GeneralSettingsComponent implements OnInit, OnDestroy {
+ private readonly fb = inject(FormBuilder);
+ private liveAnnouncer = inject(LiveAnnouncer);
+ private readonly onlyDifferentValuesValidator = inject(
+ OnlyDifferentValuesValidator
+ );
+ private settingsStore = inject(GeneralSettingsStore);
+ private readonly loaderService = inject(LoaderService);
+
+ readonly reloadSettingLink = viewChild('reloadSettingLink');
+ @Output() closeSettingEvent = new EventEmitter();
+
+ private isSettingsDisable = false;
+ get settingsDisable(): boolean {
+ return this.isSettingsDisable;
+ }
+ @Input() set settingsDisable(value: boolean) {
+ this.isSettingsDisable = value;
+ if (value) {
+ this.disableSettings();
+ } else {
+ this.enableSettings();
+ }
+ }
+ public readonly CalloutType = CalloutType;
+ public readonly EventType = EventType;
+ public readonly FormKey = FormKey;
+ public settingForm!: FormGroup;
+ viewModel$ = this.settingsStore.viewModel$;
+
+ private destroy$: Subject = new Subject();
+
+ get deviceControl(): FormControl {
+ return this.settingForm.get(FormKey.DEVICE) as FormControl;
+ }
+
+ get internetControl(): FormControl {
+ return this.settingForm.get(FormKey.INTERNET) as FormControl;
+ }
+
+ get logLevel(): FormControl {
+ return this.settingForm.get(FormKey.LOG_LEVEL) as FormControl;
+ }
+
+ get monitorPeriod(): FormControl {
+ return this.settingForm.get(FormKey.MONITOR_PERIOD) as FormControl;
+ }
+
+ get isFormValues(): boolean {
+ return (
+ this.deviceControl?.value?.value &&
+ (this.isInternetControlDisabled || this.internetControl?.value?.value)
+ );
+ }
+
+ get isInternetControlDisabled(): boolean {
+ return this.internetControl?.disabled;
+ }
+
+ get isFormError(): boolean {
+ return this.settingForm.hasError('hasSameValues');
+ }
+
+ ngOnInit() {
+ this.createSettingForm();
+ this.cleanFormErrorMessage();
+ this.settingsStore.getInterfaces();
+ this.getSystemConfig();
+ this.setDefaultFormValues();
+ }
+
+ reloadSetting(): void {
+ if (this.settingsDisable) {
+ return;
+ }
+ this.showLoading();
+ this.getSystemInterfaces();
+ this.getSystemConfig();
+ this.setDefaultFormValues();
+ }
+ closeSetting(message: string): void {
+ this.resetForm();
+ this.closeSettingEvent.emit();
+ this.liveAnnouncer.announce(
+ `The ${message} finished. The system settings panel is closed.`
+ );
+ this.setDefaultFormValues();
+ }
+
+ saveSetting(): void {
+ if (this.settingForm.invalid) {
+ this.settingsStore.setIsSubmitting(true);
+ this.settingForm.markAllAsTouched();
+ } else {
+ this.createSystemConfig();
+ }
+ }
+
+ private disableSettings(): void {
+ this.settingForm?.disable();
+ this.reloadSettingLink()?.nativeElement.setAttribute(
+ 'aria-disabled',
+ 'true'
+ );
+ }
+
+ private enableSettings(): void {
+ this.settingForm?.enable();
+ this.reloadSettingLink()?.nativeElement.removeAttribute('aria-disabled');
+ }
+
+ private createSettingForm() {
+ this.settingForm = this.fb.group(
+ {
+ device_intf: [''],
+ internet_intf: [''],
+ log_level: [''],
+ monitor_period: [''],
+ },
+ {
+ validators: [this.onlyDifferentValuesValidator.onlyDifferentSetting()],
+ updateOn: 'change',
+ }
+ );
+ }
+
+ private setDefaultFormValues() {
+ this.settingsStore.setDefaultFormValues(this.settingForm);
+ }
+
+ private cleanFormErrorMessage(): void {
+ this.settingForm.valueChanges
+ .pipe(
+ takeUntil(this.destroy$),
+ tap(() => this.settingsStore.setIsSubmitting(false))
+ )
+ .subscribe();
+ }
+
+ private createSystemConfig(): void {
+ const { device_intf, internet_intf, log_level, monitor_period } =
+ this.settingForm.value;
+ const data: SystemConfig = {
+ network: {
+ device_intf: device_intf.key,
+ internet_intf: this.isInternetControlDisabled ? '' : internet_intf.key,
+ },
+ log_level: log_level.key,
+ monitor_period: Number(monitor_period.key),
+ };
+ this.settingsStore.updateSystemConfig({
+ onSystemConfigUpdate: () => {
+ this.closeSetting(EventType.Save);
+ },
+ config: data,
+ });
+ }
+
+ private resetForm(): void {
+ this.settingForm.reset();
+ }
+
+ ngOnDestroy() {
+ this.destroy$.next(true);
+ this.destroy$.unsubscribe();
+ }
+
+ getSystemInterfaces(): void {
+ this.settingsStore.getInterfaces();
+ this.hideLoading();
+ }
+
+ getSystemConfig(): void {
+ this.settingsStore.getSystemConfig();
+ }
+
+ private showLoading() {
+ this.loaderService.setLoading(true);
+ }
+
+ private hideLoading() {
+ this.loaderService.setLoading(false);
+ }
+}
diff --git a/modules/ui/src/app/pages/settings/settings.store.spec.ts b/modules/ui/src/app/pages/general-settings/general-settings.store.spec.ts
similarity index 97%
rename from modules/ui/src/app/pages/settings/settings.store.spec.ts
rename to modules/ui/src/app/pages/general-settings/general-settings.store.spec.ts
index 969f5cb9f..451a967df 100644
--- a/modules/ui/src/app/pages/settings/settings.store.spec.ts
+++ b/modules/ui/src/app/pages/general-settings/general-settings.store.spec.ts
@@ -13,7 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { LOG_LEVELS, MONITORING_PERIOD, SettingsStore } from './settings.store';
+import {
+ LOG_LEVELS,
+ MONITORING_PERIOD,
+ GeneralSettingsStore,
+} from './general-settings.store';
import { TestRunService } from '../../services/test-run.service';
import SpyObj = jasmine.SpyObj;
import { TestBed } from '@angular/core/testing';
@@ -41,8 +45,8 @@ import {
MOCK_SYSTEM_CONFIG_WITH_SINGLE_PORT,
} from '../../mocks/settings.mock';
-describe('SettingsStore', () => {
- let settingsStore: SettingsStore;
+describe('GeneralSettingsStore', () => {
+ let settingsStore: GeneralSettingsStore;
let mockService: SpyObj;
let store: MockStore;
let fb: FormBuilder;
@@ -56,7 +60,7 @@ describe('SettingsStore', () => {
TestBed.configureTestingModule({
providers: [
- SettingsStore,
+ GeneralSettingsStore,
{ provide: TestRunService, useValue: mockService },
provideMockStore({
selectors: [
@@ -68,7 +72,7 @@ describe('SettingsStore', () => {
],
});
- settingsStore = TestBed.inject(SettingsStore);
+ settingsStore = TestBed.inject(GeneralSettingsStore);
store = TestBed.inject(MockStore);
fb = TestBed.inject(FormBuilder);
spyOn(store, 'dispatch').and.callFake(() => {});
diff --git a/modules/ui/src/app/pages/settings/settings.store.ts b/modules/ui/src/app/pages/general-settings/general-settings.store.ts
similarity index 98%
rename from modules/ui/src/app/pages/settings/settings.store.ts
rename to modules/ui/src/app/pages/general-settings/general-settings.store.ts
index 1343a1896..56e458187 100644
--- a/modules/ui/src/app/pages/settings/settings.store.ts
+++ b/modules/ui/src/app/pages/general-settings/general-settings.store.ts
@@ -67,7 +67,7 @@ export const MONITORING_PERIOD = {
600: 'Very slow device',
};
@Injectable()
-export class SettingsStore extends ComponentStore {
+export class GeneralSettingsStore extends ComponentStore {
private testRunService = inject(TestRunService);
private store = inject>(Store);
@@ -282,7 +282,7 @@ export class SettingsStore extends ComponentStore {
): void {
this.setDefaultValue(
value,
- SettingsStore.DEFAULT_LOG_LEVEL,
+ GeneralSettingsStore.DEFAULT_LOG_LEVEL,
options,
formGroup.get(FormKey.LOG_LEVEL) as FormControl
);
@@ -295,7 +295,7 @@ export class SettingsStore extends ComponentStore {
): void {
this.setDefaultValue(
value,
- SettingsStore.DEFAULT_MONITORING_PERIOD,
+ GeneralSettingsStore.DEFAULT_MONITORING_PERIOD,
options,
formGroup.get(FormKey.MONITOR_PERIOD) as FormControl
);
diff --git a/modules/ui/src/app/pages/settings/only-different-values.validator.ts b/modules/ui/src/app/pages/general-settings/only-different-values.validator.ts
similarity index 100%
rename from modules/ui/src/app/pages/settings/only-different-values.validator.ts
rename to modules/ui/src/app/pages/general-settings/only-different-values.validator.ts
diff --git a/modules/ui/src/app/pages/settings/settings.component.html b/modules/ui/src/app/pages/settings/settings.component.html
index 2dd8567b3..b1cd8a1cb 100644
--- a/modules/ui/src/app/pages/settings/settings.component.html
+++ b/modules/ui/src/app/pages/settings/settings.component.html
@@ -13,104 +13,14 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-
-
-
-
-
- Warning! No ports detected.
-
-
-
-
+
+ Settings
+
+
+
+ General content
+ Certificates content
+
diff --git a/modules/ui/src/app/pages/settings/settings.component.scss b/modules/ui/src/app/pages/settings/settings.component.scss
index ea51621ed..07154d04a 100644
--- a/modules/ui/src/app/pages/settings/settings.component.scss
+++ b/modules/ui/src/app/pages/settings/settings.component.scss
@@ -13,140 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@use '@angular/material' as mat;
-@use 'colors';
-@use 'variables';
-:host {
- display: flex;
- flex-direction: column;
- height: 100%;
- flex: 1 0 auto;
+.toolbar {
+ height: auto;
+ padding: 22px 0px 18px 32px;
+}
+.title {
+ padding: 24px 0 16px;
}
-.settings-drawer-header {
+.tab-item {
display: flex;
- justify-content: space-between;
+ padding: 0px 32px;
align-items: center;
- padding: 12px 12px 16px 24px;
-
- &-title {
- margin: 0;
- font-size: 22px;
- font-style: normal;
- font-weight: 400;
- line-height: 28px;
- color: colors.$dark-grey;
- }
-
- &-button {
- min-width: 24px;
- width: 24px;
- height: 24px;
- margin: 4px;
- padding: 8px;
- box-sizing: content-box;
- line-height: normal !important;
-
- .close-button-icon {
- width: 24px;
- height: 24px;
- margin: 0;
- }
-
- ::ng-deep * {
- line-height: inherit !important;
- }
- }
+ gap: 32px;
}
-
-.setting-drawer-content {
- padding: 0 16px 8px 16px;
- overflow: hidden;
- flex: 1;
-
- form {
- display: grid;
- height: 100%;
+.tab-group {
+ ::ng-deep .mat-mdc-tab-labels {
+ gap: 16px;
+ padding: 0 12px;
}
- .setting-drawer-content-form-empty {
- grid-template-rows: repeat(2, auto) 1fr;
+ ::ng-deep.mat-mdc-tab {
+ padding: 0px 8px;
}
}
-
-.setting-drawer-content-inputs {
- overflow: auto;
- margin: 0 -16px;
- padding: 0 16px;
-}
-
-.error-message-container {
- display: block;
- margin-top: auto;
- padding-bottom: 8px;
-}
-
-.error-message-container + .setting-drawer-footer {
- margin-top: 0;
-}
-
-.message {
- margin: 0;
- padding: 6px 0 12px 0;
- color: colors.$grey-800;
- font-family: variables.$font-secondary;
- font-size: 14px;
- line-height: 20px;
- letter-spacing: 0.2px;
-}
-
-.setting-drawer-footer {
- padding: 0 8px;
- margin-top: auto;
- display: flex;
- flex-shrink: 0;
- justify-content: flex-end;
-
- .close-button,
- .save-button {
- padding: 0 24px;
- font-size: 14px;
- font-weight: 500;
- line-height: 20px;
- letter-spacing: 0.25px;
- }
-
- .close-button {
- margin-right: 10px;
- }
-}
-
-.settings-disabled-overlay {
- position: absolute;
- width: 100%;
- left: 0;
- right: 0;
- top: 75px;
- bottom: 45px;
- background-color: rgba(255, 255, 255, 0.7);
- z-index: 2;
-}
-
-.disabled {
- .message-link {
- cursor: default;
- pointer-events: none;
-
- &:focus-visible {
- outline: none;
- }
- }
-}
-
-.settings-drawer-header-button:not(.mat-mdc-button-disabled),
-.close-button:not(.mat-mdc-button-disabled),
-.save-button:not(.mat-mdc-button-disabled) {
- cursor: pointer;
- pointer-events: auto;
-}
diff --git a/modules/ui/src/app/pages/settings/settings.component.spec.ts b/modules/ui/src/app/pages/settings/settings.component.spec.ts
index 84ab9be82..0f5d81c01 100644
--- a/modules/ui/src/app/pages/settings/settings.component.spec.ts
+++ b/modules/ui/src/app/pages/settings/settings.component.spec.ts
@@ -1,3 +1,6 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SettingsComponent } from './settings.component';
/**
* Copyright 2023 Google LLC
*
@@ -13,369 +16,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
-import { SettingsComponent } from './settings.component';
-import { of } from 'rxjs';
-import { MatRadioModule } from '@angular/material/radio';
-import { ReactiveFormsModule } from '@angular/forms';
-import { MatButtonModule } from '@angular/material/button';
-import { MatIcon, MatIconModule } from '@angular/material/icon';
-import { MatIconTestingModule } from '@angular/material/icon/testing';
-import { Component, Input } from '@angular/core';
-import { LiveAnnouncer } from '@angular/cdk/a11y';
-import SpyObj = jasmine.SpyObj;
-import { MatInputModule } from '@angular/material/input';
-import { MatSelectModule } from '@angular/material/select';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-import { provideMockStore } from '@ngrx/store/testing';
-import { LoaderService } from '../../services/loader.service';
-import { SettingsStore } from './settings.store';
-import {
- MOCK_INTERFACES,
- MOCK_SYSTEM_CONFIG_WITH_DATA,
-} from '../../mocks/settings.mock';
-import { SettingsDropdownComponent } from './components/settings-dropdown/settings-dropdown.component';
-import { CalloutComponent } from '../../components/callout/callout.component';
-import { SpinnerComponent } from '../../components/spinner/spinner.component';
-
-describe('GeneralSettingsComponent', () => {
+describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture;
- let mockLiveAnnouncer: SpyObj;
- let compiled: HTMLElement;
- let mockLoaderService: SpyObj;
- let mockSettingsStore: SpyObj;
beforeEach(async () => {
- mockLiveAnnouncer = jasmine.createSpyObj(['announce']);
- mockLoaderService = jasmine.createSpyObj('LoaderService', ['setLoading']);
- mockSettingsStore = jasmine.createSpyObj('SettingsStore', [
- 'getInterfaces',
- 'updateSystemConfig',
- 'setIsSubmitting',
- 'setDefaultFormValues',
- 'getSystemConfig',
- 'viewModel$',
- ]);
-
await TestBed.configureTestingModule({
- providers: [
- { provide: LiveAnnouncer, useValue: mockLiveAnnouncer },
- { provide: LoaderService, useValue: mockLoaderService },
- { provide: SettingsStore, useValue: mockSettingsStore },
- provideMockStore(),
- ],
- imports: [
- SettingsComponent,
- BrowserAnimationsModule,
- MatButtonModule,
- MatIconModule,
- MatRadioModule,
- ReactiveFormsModule,
- MatIconTestingModule,
- MatIcon,
- MatInputModule,
- MatSelectModule,
- SettingsDropdownComponent,
- FakeSpinnerComponent,
- FakeCalloutComponent,
- ],
- })
- .overrideComponent(SettingsComponent, {
- remove: {
- imports: [CalloutComponent, SpinnerComponent],
- },
- add: {
- imports: [FakeSpinnerComponent, FakeCalloutComponent],
- },
- })
- .compileComponents();
-
- TestBed.overrideProvider(SettingsStore, { useValue: mockSettingsStore });
+ imports: [SettingsComponent, NoopAnimationsModule],
+ }).compileComponents();
fixture = TestBed.createComponent(SettingsComponent);
-
component = fixture.componentInstance;
- component.viewModel$ = of({
- systemConfig: { network: {} },
- hasConnectionSettings: false,
- isSubmitting: false,
- isLessThanOneInterface: false,
- interfaces: {},
- deviceOptions: {},
- internetOptions: {},
- logLevelOptions: {},
- monitoringPeriodOptions: {},
- });
fixture.detectChanges();
- compiled = fixture.nativeElement as HTMLElement;
-
- component.ngOnInit();
});
it('should create', () => {
expect(component).toBeTruthy();
});
-
- it('#reloadSetting should call setLoading in loaderService', () => {
- component.reloadSetting();
-
- expect(mockLoaderService.setLoading).toHaveBeenCalledWith(true);
- });
-
- describe('#settingsDisable', () => {
- it('should disable setting form when get settingDisable as true ', () => {
- spyOn(component.settingForm, 'disable');
-
- component.settingsDisable = true;
-
- expect(component.settingForm.disable).toHaveBeenCalled();
- });
-
- it('should enable setting form when get settingDisable as false ', () => {
- spyOn(component.settingForm, 'enable');
-
- component.settingsDisable = false;
-
- expect(component.settingForm.enable).toHaveBeenCalled();
- });
-
- it('should disable "Save" button when get settingDisable as true', () => {
- component.settingsDisable = true;
-
- const saveBtn = compiled.querySelector(
- '.save-button'
- ) as HTMLButtonElement;
-
- expect(saveBtn.disabled).toBeTrue();
- });
-
- it('should disable "Refresh" link when settingDisable', () => {
- component.settingsDisable = true;
-
- const refreshLink = compiled.querySelector(
- '.message-link'
- ) as HTMLAnchorElement;
-
- refreshLink.click();
-
- expect(refreshLink.hasAttribute('aria-disabled')).toBeTrue();
- expect(mockLoaderService.setLoading).not.toHaveBeenCalled();
- });
- });
-
- describe('#closeSetting', () => {
- beforeEach(() => {
- component.ngOnInit();
- });
-
- it('should emit closeSettingEvent', () => {
- spyOn(component.closeSettingEvent, 'emit');
-
- component.closeSetting('Message');
-
- expect(component.closeSettingEvent.emit).toHaveBeenCalled();
- });
-
- it('should call liveAnnouncer with provided message', () => {
- const mockMessage = 'mock event';
-
- component.closeSetting(mockMessage);
-
- expect(mockLiveAnnouncer.announce).toHaveBeenCalledWith(
- `The ${mockMessage} finished. The system settings panel is closed.`
- );
- });
-
- it('should call reset settingForm', () => {
- spyOn(component.settingForm, 'reset');
-
- component.closeSetting('Message');
-
- expect(component.settingForm.reset).toHaveBeenCalled();
- });
-
- it('should call setDefaultFormValues', () => {
- component.closeSetting('Message');
-
- expect(mockSettingsStore.setDefaultFormValues).toHaveBeenCalled();
- });
- });
-
- describe('#saveSetting', () => {
- beforeEach(() => {
- component.ngOnInit();
- });
-
- it('should have form error if form has the same value', () => {
- const mockSameValue = 'sameValue';
- component.deviceControl.setValue(mockSameValue);
- component.internetControl.setValue(mockSameValue);
-
- component.saveSetting();
-
- expect(component.settingForm.invalid).toBeTrue();
- expect(component.isFormError).toBeTrue();
- expect(mockSettingsStore.setIsSubmitting).toHaveBeenCalledWith(true);
- });
-
- it('should call createSystemConfig when setting form valid', () => {
- const expectedResult = {
- network: {
- device_intf: 'mockDeviceKey',
- internet_intf: '',
- },
- log_level: 'INFO',
- monitor_period: 600,
- };
-
- component.deviceControl.setValue({
- key: 'mockDeviceKey',
- value: 'mockDeviceValue',
- });
-
- component.internetControl.setValue({
- key: '',
- value: 'defaultValue',
- });
-
- component.logLevel.setValue({
- key: 'INFO',
- value: '',
- });
-
- component.monitorPeriod.setValue({
- key: '600',
- value: '',
- });
-
- component.saveSetting();
-
- const args = mockSettingsStore.updateSystemConfig.calls.argsFor(0);
- // @ts-expect-error config is in object
- expect(args[0].config).toEqual(expectedResult);
- expect(component.settingForm.invalid).toBeFalse();
- expect(mockSettingsStore.updateSystemConfig).toHaveBeenCalled();
- });
- });
-
- describe('with no interfaces data', () => {
- beforeEach(() => {
- component.viewModel$ = of({
- systemConfig: { network: {} },
- hasConnectionSettings: false,
- isSubmitting: false,
- isLessThanOneInterface: false,
- interfaces: {},
- deviceOptions: {},
- internetOptions: {},
- logLevelOptions: {},
- monitoringPeriodOptions: {},
- });
- fixture.detectChanges();
- });
-
- it('should have callout component', () => {
- const callout = compiled.querySelector('app-callout');
-
- expect(callout).toBeTruthy();
- });
-
- it('should have disabled "Save" button', () => {
- const saveBtn = compiled.querySelector(
- '.save-button'
- ) as HTMLButtonElement;
-
- expect(saveBtn.disabled).toBeTrue();
- });
- });
-
- describe('with interfaces length less than one', () => {
- beforeEach(() => {
- component.viewModel$ = of({
- systemConfig: { network: {} },
- hasConnectionSettings: false,
- isSubmitting: false,
- isLessThanOneInterface: true,
- interfaces: {},
- deviceOptions: {},
- internetOptions: {},
- logLevelOptions: {},
- monitoringPeriodOptions: {},
- });
- fixture.detectChanges();
- });
-
- it('should have disabled "Save" button', () => {
- component.deviceControl.setValue(
- MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf
- );
- component.internetControl.setValue(
- MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.internet_intf
- );
- fixture.detectChanges();
-
- const saveBtn = compiled.querySelector(
- '.save-button'
- ) as HTMLButtonElement;
-
- expect(saveBtn.disabled).toBeTrue();
- });
- });
-
- describe('with interfaces length more then one', () => {
- beforeEach(() => {
- component.viewModel$ = of({
- systemConfig: { network: {} },
- hasConnectionSettings: false,
- isSubmitting: false,
- isLessThanOneInterface: false,
- interfaces: MOCK_INTERFACES,
- deviceOptions: MOCK_INTERFACES,
- internetOptions: MOCK_INTERFACES,
- logLevelOptions: {},
- monitoringPeriodOptions: {},
- });
- fixture.detectChanges();
- });
-
- it('should not have callout component', () => {
- const callout = compiled.querySelector('app-callout');
-
- expect(callout).toBeFalsy();
- });
-
- it('should not have disabled "Save" button', () => {
- component.deviceControl.setValue({
- key: MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf,
- value: 'value',
- });
- component.internetControl.setValue({
- key: MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.internet_intf,
- value: 'value',
- });
- fixture.detectChanges();
-
- const saveBtn = compiled.querySelector(
- '.save-button'
- ) as HTMLButtonElement;
-
- expect(saveBtn.disabled).toBeFalse();
- });
- });
});
-
-@Component({
- selector: 'app-spinner',
- template: '',
-})
-class FakeSpinnerComponent {}
-
-@Component({
- selector: 'app-callout',
- template: '',
-})
-class FakeCalloutComponent {
- @Input() type = '';
-}
diff --git a/modules/ui/src/app/pages/settings/settings.component.ts b/modules/ui/src/app/pages/settings/settings.component.ts
index bbd9e3f8a..53385ee1c 100644
--- a/modules/ui/src/app/pages/settings/settings.component.ts
+++ b/modules/ui/src/app/pages/settings/settings.component.ts
@@ -13,255 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {
- ChangeDetectionStrategy,
- Component,
- ElementRef,
- EventEmitter,
- Input,
- OnDestroy,
- OnInit,
- Output,
- viewChild,
- inject,
-} from '@angular/core';
-import {
- FormBuilder,
- FormControl,
- FormGroup,
- ReactiveFormsModule,
-} from '@angular/forms';
-import { Subject, takeUntil, tap } from 'rxjs';
-import { OnlyDifferentValuesValidator } from './only-different-values.validator';
-import { CalloutType } from '../../model/callout-type';
-import { CdkTrapFocus, LiveAnnouncer } from '@angular/cdk/a11y';
-import { EventType } from '../../model/event-type';
-import { FormKey, SystemConfig } from '../../model/setting';
-import { SettingsStore } from './settings.store';
-import { LoaderService } from '../../services/loader.service';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar';
-import { MatFormFieldModule } from '@angular/material/form-field';
-import { MatButtonToggleModule } from '@angular/material/button-toggle';
-import { MatInputModule } from '@angular/material/input';
-import { MatSnackBarModule } from '@angular/material/snack-bar';
-import { SpinnerComponent } from '../../components/spinner/spinner.component';
-import { MatTooltipModule } from '@angular/material/tooltip';
-import { MatButtonModule } from '@angular/material/button';
-import { SettingsDropdownComponent } from './components/settings-dropdown/settings-dropdown.component';
-import { MatSelectModule } from '@angular/material/select';
-import { MatIconModule } from '@angular/material/icon';
-import { MatSidenavModule } from '@angular/material/sidenav';
-import { MatRadioModule } from '@angular/material/radio';
-import { CalloutComponent } from '../../components/callout/callout.component';
import { CommonModule } from '@angular/common';
+import { MatTabsModule } from '@angular/material/tabs';
@Component({
selector: 'app-settings',
+ imports: [CommonModule, MatToolbarModule, MatTabsModule],
templateUrl: './settings.component.html',
- styleUrls: ['./settings.component.scss'],
- hostDirectives: [CdkTrapFocus],
- imports: [
- MatButtonModule,
- MatIconModule,
- MatToolbarModule,
- MatSidenavModule,
- MatButtonToggleModule,
- MatRadioModule,
- MatInputModule,
- MatSelectModule,
- MatTooltipModule,
- ReactiveFormsModule,
- MatFormFieldModule,
- MatSnackBarModule,
- SpinnerComponent,
- CalloutComponent,
- SettingsDropdownComponent,
- CommonModule,
- ],
- providers: [SettingsStore],
+ styleUrl: './settings.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class SettingsComponent implements OnInit, OnDestroy {
- private readonly fb = inject(FormBuilder);
- private liveAnnouncer = inject(LiveAnnouncer);
- private readonly onlyDifferentValuesValidator = inject(
- OnlyDifferentValuesValidator
- );
- private settingsStore = inject(SettingsStore);
- private readonly loaderService = inject(LoaderService);
-
- readonly reloadSettingLink = viewChild('reloadSettingLink');
- @Output() closeSettingEvent = new EventEmitter();
-
- private isSettingsDisable = false;
- get settingsDisable(): boolean {
- return this.isSettingsDisable;
- }
- @Input() set settingsDisable(value: boolean) {
- this.isSettingsDisable = value;
- if (value) {
- this.disableSettings();
- } else {
- this.enableSettings();
- }
- }
- public readonly CalloutType = CalloutType;
- public readonly EventType = EventType;
- public readonly FormKey = FormKey;
- public settingForm!: FormGroup;
- viewModel$ = this.settingsStore.viewModel$;
-
- private destroy$: Subject = new Subject();
-
- get deviceControl(): FormControl {
- return this.settingForm.get(FormKey.DEVICE) as FormControl;
- }
-
- get internetControl(): FormControl {
- return this.settingForm.get(FormKey.INTERNET) as FormControl;
- }
-
- get logLevel(): FormControl {
- return this.settingForm.get(FormKey.LOG_LEVEL) as FormControl;
- }
-
- get monitorPeriod(): FormControl {
- return this.settingForm.get(FormKey.MONITOR_PERIOD) as FormControl;
- }
-
- get isFormValues(): boolean {
- return (
- this.deviceControl?.value?.value &&
- (this.isInternetControlDisabled || this.internetControl?.value?.value)
- );
- }
-
- get isInternetControlDisabled(): boolean {
- return this.internetControl?.disabled;
- }
-
- get isFormError(): boolean {
- return this.settingForm.hasError('hasSameValues');
- }
-
- ngOnInit() {
- this.createSettingForm();
- this.cleanFormErrorMessage();
- this.settingsStore.getInterfaces();
- this.getSystemConfig();
- this.setDefaultFormValues();
- }
-
- reloadSetting(): void {
- if (this.settingsDisable) {
- return;
- }
- this.showLoading();
- this.getSystemInterfaces();
- this.getSystemConfig();
- this.setDefaultFormValues();
- }
- closeSetting(message: string): void {
- this.resetForm();
- this.closeSettingEvent.emit();
- this.liveAnnouncer.announce(
- `The ${message} finished. The system settings panel is closed.`
- );
- this.setDefaultFormValues();
- }
-
- saveSetting(): void {
- if (this.settingForm.invalid) {
- this.settingsStore.setIsSubmitting(true);
- this.settingForm.markAllAsTouched();
- } else {
- this.createSystemConfig();
- }
- }
-
- private disableSettings(): void {
- this.settingForm?.disable();
- this.reloadSettingLink()?.nativeElement.setAttribute(
- 'aria-disabled',
- 'true'
- );
- }
-
- private enableSettings(): void {
- this.settingForm?.enable();
- this.reloadSettingLink()?.nativeElement.removeAttribute('aria-disabled');
- }
-
- private createSettingForm() {
- this.settingForm = this.fb.group(
- {
- device_intf: [''],
- internet_intf: [''],
- log_level: [''],
- monitor_period: [''],
- },
- {
- validators: [this.onlyDifferentValuesValidator.onlyDifferentSetting()],
- updateOn: 'change',
- }
- );
- }
-
- private setDefaultFormValues() {
- this.settingsStore.setDefaultFormValues(this.settingForm);
- }
-
- private cleanFormErrorMessage(): void {
- this.settingForm.valueChanges
- .pipe(
- takeUntil(this.destroy$),
- tap(() => this.settingsStore.setIsSubmitting(false))
- )
- .subscribe();
- }
-
- private createSystemConfig(): void {
- const { device_intf, internet_intf, log_level, monitor_period } =
- this.settingForm.value;
- const data: SystemConfig = {
- network: {
- device_intf: device_intf.key,
- internet_intf: this.isInternetControlDisabled ? '' : internet_intf.key,
- },
- log_level: log_level.key,
- monitor_period: Number(monitor_period.key),
- };
- this.settingsStore.updateSystemConfig({
- onSystemConfigUpdate: () => {
- this.closeSetting(EventType.Save);
- },
- config: data,
- });
- }
-
- private resetForm(): void {
- this.settingForm.reset();
- }
-
- ngOnDestroy() {
- this.destroy$.next(true);
- this.destroy$.unsubscribe();
- }
-
- getSystemInterfaces(): void {
- this.settingsStore.getInterfaces();
- this.hideLoading();
- }
-
- getSystemConfig(): void {
- this.settingsStore.getSystemConfig();
- }
-
- private showLoading() {
- this.loaderService.setLoading(true);
- }
-
- private hideLoading() {
- this.loaderService.setLoading(false);
- }
-}
+export class SettingsComponent {}
diff --git a/modules/ui/src/styles.scss b/modules/ui/src/styles.scss
index 916d6f0d2..faf90c53f 100644
--- a/modules/ui/src/styles.scss
+++ b/modules/ui/src/styles.scss
@@ -42,6 +42,16 @@
supporting-text-size: 14px,
)
);
+
+ @include mat.tabs-overrides(
+ (
+ inactive-label-text-color: colors.$on-surface-variant,
+ active-label-text-color: colors.$secondary,
+ active-focus-label-text-color: colors.$secondary,
+ label-text-font: variables.$font-text,
+ container-height: 20px,
+ )
+ );
}
.filter-result {
@@ -132,6 +142,15 @@ mat-hint {
display: none;
}
+.mat-mdc-tab.mat-focus-indicator {
+ &:focus::before {
+ content: none;
+ }
+ &:focus-visible {
+ border: 1px solid colors.$black;
+ }
+}
+
.mat-sort-header-container.mat-focus-indicator:focus::before {
content: none;
}
@@ -149,6 +168,22 @@ mat-hint {
display: none;
}
}
+
+.app-toolbar-button.app-sidebar-button-active {
+ .mat-mdc-button-persistent-ripple::before {
+ opacity: 1;
+ background: colors.$light-grey;
+ }
+
+ &:hover,
+ &:focus-visible {
+ .mat-mdc-button-persistent-ripple::before {
+ opacity: 0.6;
+ background: colors.$grey-100;
+ }
+ }
+}
+
.app-sidebar-button {
&.mat-mdc-button:has(.material-icons, mat-icon, [matButtonIcon]) {
padding: 6px;
@@ -248,6 +283,7 @@ mat-hint {
font-weight: 400;
line-height: 40px;
letter-spacing: 0;
+ color: colors.$on-surface;
}
.message-link {