Skip to content

Commit

Permalink
feat(chrome-ext): support states
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieu-crouzet committed May 17, 2024
1 parent 71d1e62 commit 6bdd008
Show file tree
Hide file tree
Showing 35 changed files with 1,365 additions and 254 deletions.
2 changes: 2 additions & 0 deletions apps/chrome-devtools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ The extension comes with the following features:
- **Visual Testing toggle**
- **Rule Engine current state**: Rule engine state, rule engine logs, etc.
- **Configuration**: display and modification of the application components configuration.
- **Theming**: display and modification of the application theming variables.
- **States**: save customization to be able to apply it later or share it.
3 changes: 2 additions & 1 deletion apps/chrome-devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"build": "yarn nx build chrome-devtools",
"postbuild:patch": "yarn patch:package && yarn patch:manifest && yarn patch:extension",
"patch:package": "cpy 'package.json' 'dist' && patch-package-json-main",
"copy:assets": "cpy 'src/manifest.json' 'src/devtools.html' dist --flat && cpy './src/assets/**' dist",
"copy:assets": "cpy 'src/manifest.json' 'src/devtools.html' 'src/options.html' dist --flat && cpy './src/assets/**' dist",
"patch:manifest": "node scripts/sanitize-manifest.cjs",
"build:extension": "tsc -b tsconfig.extension.json",
"patch:extension": "node scripts/sanitize-extension.cjs && node scripts/set-manifest-version.cjs",
Expand Down Expand Up @@ -89,6 +89,7 @@
"@angular/router": "~17.3.0",
"@design-factory/design-factory": "~17.1.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@ng-select/ng-select": "^12.0.7",
"@ngrx/entity": "~17.2.0",
"@ngrx/store": "~17.2.0",
"@o3r/application": "workspace:^",
Expand Down
43 changes: 43 additions & 0 deletions apps/chrome-devtools/schemas/state.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "OtterDevtoolsChromeExtensionState",
"description": "Schema of Otter Devtools chrome extension state",
"type": "object",
"required": [
"color",
"colorContrast",
"name"
],
"properties": {
"color": {
"type": "string"
},
"colorContrast": {
"type": "string"
},
"name": {
"type": "string"
},
"configurations": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"localizations": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"stylingVariables": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
111 changes: 72 additions & 39 deletions apps/chrome-devtools/src/app-devtools/app.component.html
Original file line number Diff line number Diff line change
@@ -1,42 +1,75 @@
<app-connection>
<ul ngbNav #nav="ngbNav" class="nav-tabs px-2 pt-2 navbar-light bg-light">
<li [ngbNavItem]="1">
<a ngbNavLink>General</a>
<ng-template ngbNavContent>
<o3r-debug-panel-pres></o3r-debug-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="2">
<a ngbNavLink>Rules Engine</a>
<ng-template ngbNavContent>
<o3r-ruleset-history-pres [rulesetExecutions]="rulesetExecutions$ | async"></o3r-ruleset-history-pres>
</ng-template>
</li>
<li [ngbNavItem]="3">
<a ngbNavLink>Configuration</a>
<ng-template ngbNavContent>
<o3r-config-panel-pres></o3r-config-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="4">
<a ngbNavLink>Component</a>
<ng-template ngbNavContent>
<o3r-component-panel-pres></o3r-component-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="5">
<a ngbNavLink>Localization</a>
<ng-template ngbNavContent>
<o3r-localization-panel-pres></o3r-localization-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="6">
<a ngbNavLink>Theming</a>
<ng-template ngbNavContent>
<o3r-theming-panel-pres></o3r-theming-panel-pres>
</ng-template>
</li>
</ul>

<div class="d-flex align-items-stretch">
<ul ngbNav #nav="ngbNav" class="nav-tabs px-2 pt-2 navbar-light bg-light flex-fill">
<li [ngbNavItem]="1">
<a ngbNavLink>General</a>
<ng-template ngbNavContent>
<o3r-debug-panel-pres></o3r-debug-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="2">
<a ngbNavLink>Rules Engine</a>
<ng-template ngbNavContent>
<o3r-ruleset-history-pres [rulesetExecutions]="rulesetExecutions$ | async"></o3r-ruleset-history-pres>
</ng-template>
</li>
<li [ngbNavItem]="3">
<a ngbNavLink>Configuration</a>
<ng-template ngbNavContent>
<o3r-config-panel-pres></o3r-config-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="4">
<a ngbNavLink>Component</a>
<ng-template ngbNavContent>
<o3r-component-panel-pres></o3r-component-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="5">
<a ngbNavLink>Localization</a>
<ng-template ngbNavContent>
<o3r-localization-panel-pres></o3r-localization-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="6">
<a ngbNavLink>Theming</a>
<ng-template ngbNavContent>
<o3r-theming-panel-pres></o3r-theming-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="7">
<a ngbNavLink>States</a>
<ng-template ngbNavContent>
<o3r-state-panel></o3r-state-panel>
</ng-template>
</li>
</ul>
<form [formGroup]="form" class="px-4 pt-{{hasLocalChanges() ? '2' : '3'}} ng-pristine ng-valid ng-touched border-bottom ms-auto">
<div>
<ng-select
[ngbTooltip]="
hasLocalChanges()
? 'There are some local changes unsaved'
: null
"
[style.width]="'250px'" [class.local-change]="hasLocalChanges()"
formControlName="activeStateName" placeholder="No selected state"
[items]="states()" [searchable]="false" [markFirst]="false" [compareWith]="stateCompareWithFn" bindLabel="name" bindValue="name">
<ng-template ng-label-tmp let-item="item">
<div class="d-flex align-items-baseline gap-2">
<span class="rounded-circle" [style.height]="'10px'" [style.width]="'10px'" [style.background]="item.color"></span>
<span>{{item.name}}</span>
</div>
</ng-template>
<ng-template ng-option-tmp let-item="item">
<div class="d-flex align-items-baseline gap-2">
<span class="rounded-circle" [style.height]="'10px'" [style.width]="'10px'" [style.background]="item.color"></span>
<span>{{item.name}}</span>
</div>
</ng-template>
</ng-select>
</div>
</form>
</div>
<div [ngbNavOutlet]="nav" class="mt-2 px-2"></div>
</app-connection>
80 changes: 55 additions & 25 deletions apps/chrome-devtools/src/app-devtools/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
import { AsyncPipe, JsonPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, effect, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DfSelectModule, DfTooltipModule } from '@design-factory/design-factory';
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
import { type RulesetExecutionDebug, RulesetHistoryPresModule } from '@o3r/rules-engine';
import { Observable, Subscription } from 'rxjs';
import { RulesetHistoryPresModule } from '@o3r/rules-engine';
import { AppConnectionComponent } from '../components/app-connection/app-connection.component';
import { ComponentPanelPresComponent } from './component-panel/component-panel-pres.component';
import type { State } from '../extension/interface';
import { StateService } from '../services';
import { ChromeExtensionConnectionService, isApplicationInformationMessage } from '../services/connection.service';
import { RulesetHistoryService } from '../services/ruleset-history.service';
import { ComponentPanelPresComponent } from './component-panel/component-panel-pres.component';
import { ConfigPanelPresComponent } from './config-panel/config-panel-pres.component';
import { DebugPanelPresComponent } from './debug-panel/debug-panel-pres.component';
import { DebugPanelService } from './debug-panel/debug-panel.service';
import { LocalizationPanelPresComponent } from './localization-panel/localization-panel-pres.component';
import { StatePanelComponent } from './state-panel/state-panel.component';
import { ThemingPanelPresComponent } from './theming-panel/theming-panel-pres.component';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styles: `
::ng-deep ng-select.local-change .ng-select-container {
border-color: var(--bs-recommend-warning-color);
border-width: medium;
box-sizing: content-box;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
Expand All @@ -27,32 +39,50 @@ import { ThemingPanelPresComponent } from './theming-panel/theming-panel-pres.co
AppConnectionComponent,
LocalizationPanelPresComponent,
ThemingPanelPresComponent,
AsyncPipe
AsyncPipe,
StatePanelComponent,
FormsModule,
ReactiveFormsModule,
DfSelectModule,
DfTooltipModule,
JsonPipe
]
})
export class AppComponent implements OnDestroy {
private readonly subscription = new Subscription();
export class AppComponent {
private readonly stateService = inject(StateService);
private readonly formBuilder = inject(FormBuilder);
private readonly connectionService = inject(ChromeExtensionConnectionService);
private readonly debugPanelService = inject(DebugPanelService);
private readonly rulesetHistoryService = inject(RulesetHistoryService);

public rulesetExecutions$: Observable<RulesetExecutionDebug[]>;
public readonly activeStateName = computed(() => this.stateService.activeState()?.name);
public readonly states = computed(() => Object.values(this.stateService.states()));
public readonly hasLocalChanges = this.stateService.hasLocalChanges;
public form = this.formBuilder.group({
activeStateName: new FormControl(this.activeStateName())
});

constructor(
connectionService: ChromeExtensionConnectionService,
debugPanelService: DebugPanelService,
rulesetHistoryService: RulesetHistoryService
) {
this.rulesetExecutions$ = rulesetHistoryService.rulesetExecutions$;
public rulesetExecutions$ = this.rulesetHistoryService.rulesetExecutions$;

this.subscription.add(
connectionService.message$.subscribe((message) => {
if (isApplicationInformationMessage(message)) {
debugPanelService.update(message);
}
})
);
constructor() {
effect(() => {
this.form.controls.activeStateName.setValue(this.activeStateName(), { emitEvent: false });
});
const message = toSignal(this.connectionService.message$);
effect(() => {
const msg = message();
if (isApplicationInformationMessage(msg)) {
this.debugPanelService.update(msg);
}
});
const activateStateNameFormValueChanges = toSignal(this.form.controls.activeStateName.valueChanges, { initialValue: this.activeStateName() });
effect(() => {
const stateName = activateStateNameFormValueChanges();
void this.stateService.setActiveState(stateName);
}, { allowSignalWrites: true });
}

/** @inheritDoc */
public ngOnDestroy() {
this.subscription.unsubscribe();
public stateCompareWithFn(state: State, selectedStateName: string) {
return state.name === selectedStateName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angul
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import { ConfigurationModel } from '@o3r/configuration';
import { combineLatest, Observable } from 'rxjs';
import { filter, map, startWith } from 'rxjs/operators';
import { map, startWith } from 'rxjs/operators';
import { ConfigFormComponent } from '../../components/config-form/config-form.component';
import { ChromeExtensionConnectionService, isConfigurationsMessage } from '../../services/connection.service';
import { ChromeExtensionConnectionService, filterAndMapMessage, isConfigurationsMessage } from '../../services/connection.service';

@Component({
selector: 'o3r-config-panel-pres',
Expand Down Expand Up @@ -37,9 +37,10 @@ export class ConfigPanelPresComponent {
}
);
const configs$ = connectionService.message$.pipe(
filter(isConfigurationsMessage),
map((message) => Object.values(message.configurations)
.filter((config): config is ConfigurationModel => !!config)
filterAndMapMessage(
isConfigurationsMessage,
(message) => Object.values(message.configurations)
.filter((config): config is ConfigurationModel => !!config)
)
);
this.filteredConfigs$ = combineLatest([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<h4 class="d-inline-block">Information</h4>
<button type="button" class="btn btn-link mb-2 text-decoration-none" (click)="refreshInfo()">&#10227;</button>
<ul>
<li>App name: <b>{{info.appName}}</b></li>
<li>App version: <b>{{info.appVersion}}</b></li>
<li>Mode: <b>{{info.isProduction ? 'Production' : 'Development'}}</b></li>
@if (info.sessionId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ApplicationInformationContentMessage } from '@o3r/application';
import { ReplaySubject } from 'rxjs';

export interface ExtendedApplicationInformation {
appName: string;
appVersion: string;
sessionId?: string;
sessionGeneratedTime?: Date;
Expand All @@ -26,6 +27,7 @@ export class DebugPanelService {
*/
public update(message: ApplicationInformationContentMessage) {
this.applicationInformationSubject.next({
appName: message.appName,
appVersion: message.appVersion,
sessionId: message.session?.id,
sessionGeneratedTime: message.session?.generatedTime,
Expand Down
Loading

0 comments on commit 6bdd008

Please sign in to comment.