TD-380: Create, edit, remove domain objects with forms (#128)

This commit is contained in:
Rinat Arsaev 2022-09-06 17:14:45 +03:00 committed by GitHub
parent 4e1b658e05
commit 1ebdc8b336
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
162 changed files with 1764 additions and 2739 deletions

5
.gitignore vendored
View File

@ -47,6 +47,5 @@ Thumbs.db
.angular
# Env and configs
.env
/src/assets/appConfig.json
/src/assets/authConfig.json
.env*
/src/assets/*Config*.json

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="App Stage Server" type="js.build_tools.npm" activateToolWindowBeforeRun="false">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="stage" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

7
.run/Stage.run.xml Normal file
View File

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Stage" type="CompoundRunConfigurationType">
<toRun name="Libs Dev Server" type="js.build_tools.npm" />
<toRun name="App Stage Server" type="js.build_tools.npm" />
<method v="2" />
</configuration>
</component>

View File

@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start" type="CompoundRunConfigurationType">
<toRun name="App Dev Server" type="js.build_tools.npm" />
<toRun name="Libs Dev Server" type="js.build_tools.npm" />
<toRun name="App Dev Server" type="js.build_tools.npm" />
<method v="2" />
</configuration>
</component>

View File

@ -33,6 +33,14 @@
npm ci
```
### Stage
Running in stage mode needs files:
- `env.stage`
- `src/assets/appConfig.stage.json`
- `src/assets/authConfig.stage.json`
## Usage
1. Start

View File

@ -26,7 +26,7 @@
"short-uuid",
"js-sha256",
"jwt-decode",
"element-resize-detector",
"css-element-queries",
"base64-js",
"@vality/deanonimus-proto",
"@vality/domain-proto",
@ -94,6 +94,14 @@
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"stage": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.stage.ts"
}
]
}
},
"defaultConfiguration": "production"
@ -106,6 +114,9 @@
},
"development": {
"browserTarget": "control-center:build:development"
},
"stage": {
"browserTarget": "control-center:build:development,stage"
}
},
"defaultConfiguration": "development"

141
package-lock.json generated
View File

@ -43,6 +43,7 @@
"angular-file": "3.6.0",
"angular2-prettyjson": "3.0.1",
"coerce-property": "0.3.2",
"css-element-queries": "1.2.3",
"element-resize-detector": "1.2.4",
"humanize-duration": "3.21.0",
"jsonc-parser": "2.0.2",
@ -79,6 +80,7 @@
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"concurrently": "7.3.0",
"cross-env": "7.0.3",
"dotenv": "16.0.0",
"eslint": "8.20.0",
"eslint-config-prettier": "8.5.0",
@ -9594,6 +9596,83 @@
"pretty-bytes": "^5.3.0"
}
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-env/node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/cross-env/node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/cross-env/node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cross-env/node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/cross-env/node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@ -9659,6 +9738,11 @@
"postcss": "^8.4"
}
},
"node_modules/css-element-queries": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/css-element-queries/-/css-element-queries-1.2.3.tgz",
"integrity": "sha512-QK9uovYmKTsV2GXWQiMOByVNrLn2qz6m3P7vWpOR4IdD6I3iXoDw5qtgJEN3Xq7gIbdHVKvzHjdAtcl+4Arc4Q=="
},
"node_modules/css-has-pseudo": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz",
@ -29773,6 +29857,58 @@
"pretty-bytes": "^5.3.0"
}
},
"cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.1"
},
"dependencies": {
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@ -29847,6 +29983,11 @@
"postcss-selector-parser": "^6.0.9"
}
},
"css-element-queries": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/css-element-queries/-/css-element-queries-1.2.3.tgz",
"integrity": "sha512-QK9uovYmKTsV2GXWQiMOByVNrLn2qz6m3P7vWpOR4IdD6I3iXoDw5qtgJEN3Xq7gIbdHVKvzHjdAtcl+4Arc4Q=="
},
"css-has-pseudo": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz",

View File

@ -6,12 +6,13 @@
"postinstall": "ngcc",
"start": "concurrently -n LIB,APP -c magenta,cyan npm:dev-libs \"sleep 0.5 && npm run dev\"",
"dev": "ng serve --proxy-config proxy.conf.js --port 4200",
"stage": "cross-env NODE_ENV=stage npm run dev -- --configuration=stage",
"dev-libs": "ng build ng-core --watch",
"build-app": "ng build --extra-webpack-config webpack.extra.js",
"build-libs": "ng build ng-core",
"build": "npm run build-libs && npm run build-app",
"test": "ng test",
"lint": "eslint \"src/**/*.{ts,js,html}\" --max-warnings 1033",
"lint": "eslint \"src/**/*.{ts,js,html}\" --max-warnings 886",
"lint-fix": "npm run lint -- --fix",
"lint-errors": "npm run lint -- --quiet",
"lint-libs": "eslint \"projects/**/*.{ts,js,html}\" --max-warnings 0",
@ -56,6 +57,7 @@
"angular-file": "3.6.0",
"angular2-prettyjson": "3.0.1",
"coerce-property": "0.3.2",
"css-element-queries": "1.2.3",
"element-resize-detector": "1.2.4",
"humanize-duration": "3.21.0",
"jsonc-parser": "2.0.2",
@ -92,6 +94,7 @@
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"concurrently": "7.3.0",
"cross-env": "7.0.3",
"dotenv": "16.0.0",
"eslint": "8.20.0",
"eslint-config-prettier": "8.5.0",

View File

@ -3,7 +3,7 @@
fxFlex="grow"
fxLayout="column"
fxLayout.gt-sm="row"
fxLayoutAlign="space-between"
fxLayoutAlign="space-between center"
fxLayoutGap="24px"
>
<ng-content></ng-content>

View File

@ -1,2 +1,3 @@
export * from './components';
export * from './utils';
export * from './pipes';

View File

@ -0,0 +1,2 @@
export * from './pipes.module';
export * from './inline-json.pipe';

View File

@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
import { inlineJson } from '../utils';
@Pipe({
name: 'inlineJson',
})
export class InlineJsonPipe implements PipeTransform {
transform(value: unknown, maxReadableLever: number | false = 1): unknown {
return inlineJson(value, maxReadableLever === false ? Infinity : maxReadableLever);
}
}

View File

@ -0,0 +1,11 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { InlineJsonPipe } from './inline-json.pipe';
@NgModule({
declarations: [InlineJsonPipe],
exports: [InlineJsonPipe],
imports: [CommonModule],
})
export class PipesModule {}

View File

@ -1 +1,2 @@
export * from './clean';
export * from './inline-json';

View File

@ -0,0 +1,32 @@
import { isPrimitive } from 'utility-types';
export function inlineJson(value: unknown, maxReadableLever = 1): string {
if (value === '') {
return "''";
}
if (isPrimitive(value)) {
return String(value);
}
if (Array.isArray(value) || value instanceof Map || value instanceof Set) {
const content =
maxReadableLever > 0
? Array.from(value)
.map((v) => inlineJson(v, maxReadableLever))
.join(', ')
: Array.from(value).length
? '…'
: '';
if (value instanceof Set) return ['Set(', content, ')'].filter(Boolean).join('');
if (value instanceof Map) return ['Map(', content, ')'].filter(Boolean).join('');
return ['[', content, ']'].filter(Boolean).join('');
}
const content =
maxReadableLever > 0
? ' ' +
Object.entries(value as never)
.map(([k, v]) => `${k}: ${inlineJson(v, maxReadableLever - 1)}`)
.join(', ') +
' '
: '';
return ['{', content, '}'].filter(Boolean).join('');
}

View File

@ -1,4 +1,4 @@
require('dotenv').config();
require('dotenv').config({ path: ['.env', process.env.NODE_ENV].filter(Boolean).join('.') });
const THRIFT_PROXY_CONFIG = {
context: [

View File

@ -6,8 +6,15 @@
<mat-menu #userMenu="matMenu"> <button mat-menu-item (click)="logout()">Logout</button> </mat-menu>
<mat-sidenav-container>
<mat-sidenav #sidenav fixedInViewport="true" fixedTopGap="64" mode="side" role="navigation">
<mat-sidenav-container autosize>
<mat-sidenav
#sidenav
[(opened)]="opened"
fixedInViewport="true"
fixedTopGap="64"
mode="side"
role="navigation"
>
<mat-nav-list>
<mat-list-item
*ngFor="let item of menuItems"

View File

@ -11,6 +11,8 @@ import {
PayoutRole,
} from '@cc/app/shared/services';
const SIDENAV_OPENED_KEY = 'sidenav-opened';
@Component({
selector: 'cc-root',
templateUrl: './app.component.html',
@ -18,9 +20,15 @@ import {
})
export class AppComponent implements OnInit {
username: string;
menuItems: { name: string; route: string }[] = [];
get opened(): boolean {
return localStorage.getItem(SIDENAV_OPENED_KEY) === String(true);
}
set opened(opened: boolean) {
localStorage.setItem(SIDENAV_OPENED_KEY, String(opened));
}
constructor(
private keycloakService: KeycloakService,
private appAuthGuardService: AppAuthGuardService

View File

@ -1,6 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
import { AppConfig } from './types/app-config';
@Injectable()
@ -11,7 +12,7 @@ export class ConfigService {
load(): Promise<void> {
return new Promise((resolve) => {
this.http.get<AppConfig>('assets/appConfig.json').subscribe((config) => {
this.http.get<AppConfig>(environment.appConfigPath).subscribe((config) => {
this.config = config;
resolve(undefined);
});

View File

@ -3,6 +3,7 @@ import { HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';
import { environment } from '../../environments/environment';
import { KeycloakTokenInfoService } from '../keycloak-token-info.service';
import { ConfigService } from './config.service';
@ -18,7 +19,7 @@ const initializer =
Promise.all([
keycloak
.init({
config: '/assets/authConfig.json',
config: environment.authConfigPath,
initOptions: {
onLoad: 'login-required',
checkLoginIframe: true,

View File

@ -1,234 +0,0 @@
import Int64 from '@vality/thrift-ts/lib/int64';
import {
ArrayASTNode,
ASTNode,
BooleanASTNode,
NumberASTNode,
ObjectASTNode,
PropertyASTNode,
StringASTNode,
} from '../jsonc';
import {
MetaCollection,
MetaEnum,
MetaField,
MetaMap,
MetaPrimitive,
MetaStruct,
MetaType,
MetaTyped,
MetaUnion,
PrimitiveType,
} from './model';
export interface ApplyPayload {
applied: MetaStruct | MetaUnion;
errors: string[];
}
function applyToCollection(meta: MetaCollection, nodeValue: ASTNode): MetaCollection {
if (nodeValue.type !== 'array') {
throw new Error(
`Applied to collection node value should be array type. Current type is ${nodeValue.type}`
);
}
const { items } = nodeValue as ArrayASTNode;
return {
...meta,
value: items.map((i) => applyToMeta(meta.itemMeta as MetaTyped, i)),
};
}
function applyToMap(meta: MetaMap, nodeValue: ASTNode): MetaMap {
if (nodeValue.type !== 'array') {
throw new Error(
`Applied to map node value should be array type. Current type is ${nodeValue.type}`
);
}
const { items } = nodeValue as ArrayASTNode;
const value = new Map();
for (const nodeItem of items) {
if (nodeItem.type !== 'object') {
throw new Error(
`Applied to map item node value should be object type. Current type is ${nodeItem.type}`
);
}
const keyValueProps = (nodeItem as ObjectASTNode).properties;
if (keyValueProps.length !== 2) {
throw new Error(
`Applied to map object props should has length 2 (key, value). Current length is ${keyValueProps.length}`
);
}
const [keyProp, valueProp] = keyValueProps;
const mapKey = applyToMeta(meta.keyMeta as MetaTyped, keyProp.value);
const mapValue = applyToMeta(meta.valueMeta as MetaTyped, valueProp.value);
value.set(mapKey, mapValue);
}
return {
...meta,
value,
};
}
function applyToEnum(meta: MetaEnum, nodeValue: ASTNode): MetaEnum {
if (nodeValue.type !== 'number') {
throw new Error(
`Applied to enum node value should be number type. Current type is ${nodeValue.type}`
);
}
return {
...meta,
value: (nodeValue as NumberASTNode).value,
};
}
function applyToNumber({ primitiveType }: MetaPrimitive, { value }: NumberASTNode) {
switch (primitiveType) {
case PrimitiveType.i8:
case PrimitiveType.i16:
case PrimitiveType.i32:
return value;
case PrimitiveType.i64:
return new Int64(value);
default:
throw new Error(`Wrong primitiveType ${primitiveType} and number applied value`);
}
}
function applyToString({ primitiveType }: MetaPrimitive, { value }: StringASTNode): string {
if (primitiveType !== PrimitiveType.string) {
throw new Error(`Wrong primitiveType ${primitiveType} and string applied value`);
}
return value;
}
function applyToBoolean({ primitiveType }: MetaPrimitive, nodeValue: BooleanASTNode) {
if (primitiveType !== PrimitiveType.bool) {
throw new Error(`Wrong primitiveType ${primitiveType} and boolean applied value`);
}
return nodeValue.getValue();
}
function applyToPrimitive(meta: MetaPrimitive, nodeValue: ASTNode): MetaPrimitive {
if (
nodeValue.type !== 'number' &&
nodeValue.type !== 'string' &&
nodeValue.type !== 'boolean' &&
nodeValue.type !== 'null'
) {
throw new Error(
`Applied to primitive node value should be number, string, boolean or null type. Current type is ${nodeValue.type}`
);
}
let value;
switch (nodeValue.type) {
case 'number':
value = applyToNumber(meta, nodeValue as NumberASTNode);
break;
case 'string':
value = applyToString(meta, nodeValue as StringASTNode);
break;
case 'boolean':
value = applyToBoolean(meta, nodeValue as BooleanASTNode);
break;
case 'null':
value = null;
break;
}
return {
...meta,
value,
};
}
function applyToMeta(meta: MetaTyped, value: ASTNode): MetaTyped {
switch (meta.type) {
case MetaType.struct:
return applyToStruct(meta as MetaStruct, value);
case MetaType.union:
return applyToUnion(meta as MetaUnion, value);
case MetaType.collection:
return applyToCollection(meta as MetaCollection, value);
case MetaType.map:
return applyToMap(meta as MetaMap, value);
case MetaType.enum:
return applyToEnum(meta as MetaEnum, value);
case MetaType.primitive:
return applyToPrimitive(meta as MetaPrimitive, value);
}
throw new Error(`Unsupported meta type ${meta.type}`);
}
function applyToField(field: MetaField, properties: PropertyASTNode[]): MetaField {
const found = properties.find(({ value: { location } }) => location === field.name);
return found
? {
...field,
meta: applyToMeta(field.meta as MetaTyped, found.value),
}
: field;
}
function applyToFields(fields: MetaField[], values: PropertyASTNode[]): MetaField[] {
return fields.map((f) => applyToField(f, values));
}
function applyToUnion(subject: MetaUnion, value: ASTNode): MetaUnion {
if (value.type !== 'object') {
throw new Error(
`Applied to union node value should be object type. Current type is ${value.type}`
);
}
const properties = (value as ObjectASTNode).properties;
if (properties.length !== 1) {
throw new Error(
`Applied to union node properties length should be 1. Current properties length is ${properties.length}`
);
}
return {
...subject,
settedField: properties[0].location as string,
fields: applyToFields(subject.fields, properties),
virgin: false,
};
}
function applyToStruct(subject: MetaStruct, value: ASTNode): MetaStruct {
if (value.type !== 'object') {
throw new Error(
`Applied to struct node value should be object type. Current type is ${value.type}`
);
}
return {
...subject,
fields: applyToFields(subject.fields, (value as ObjectASTNode).properties),
virgin: false,
};
}
export function applyValue(subject: MetaStruct | MetaUnion, value: ObjectASTNode): ApplyPayload {
const result = { applied: null, errors: [] };
try {
if (value.type !== 'object') {
throw new Error('Apply value must be ObjectASTNode');
}
let applied;
switch (subject.type) {
case MetaType.struct:
applied = applyToStruct(subject as MetaStruct, value);
break;
case MetaType.union:
applied = applyToUnion(subject as MetaUnion, value);
break;
default:
throw new Error(
`Applied meta type should be struct or union. Current meta type is ${subject.type}`
);
}
return { ...result, applied };
} catch (ex) {
console.error(ex);
return { ...result, errors: [ex.message] };
}
}

View File

@ -1,11 +0,0 @@
import { NgModule } from '@angular/core';
import { DefinitionService } from './definition.service';
import { MetaApplicator } from './meta-applicator.service';
import { MetaBuilder } from './meta-builder.service';
import { ThriftBuilderService } from './thrift-builder.service';
@NgModule({
providers: [DefinitionService, MetaBuilder, MetaApplicator, ThriftBuilderService],
})
export class DamselMetaModule {}

View File

@ -1,41 +0,0 @@
import { Injectable } from '@angular/core';
import { Reference } from '@vality/domain-proto';
import { Field } from '@vality/thrift-ts';
import { from, Observable, of } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { ASTDefinition } from './model';
@Injectable()
export class DefinitionService {
private def$ = from(
import('@vality/domain-proto/lib/metadata.json').then((m) => m.default)
).pipe(shareReplay(1)) as Observable<ASTDefinition[]>;
get astDefinition(): Observable<ASTDefinition[]> {
return this.def$;
}
getDomainObjectType(ref: Reference): Observable<string | null> {
if (!ref) {
return of(null);
}
const keys = Object.keys(ref);
if (keys.length !== 1) {
return of(null);
}
const searchName = keys[0];
return this.getDomainDef().pipe(
map((d) => {
const found = d.find(({ name }) => name === searchName);
return found ? (found.type as string) : null;
})
);
}
getDomainDef(): Observable<Field[]> {
return this.def$.pipe(
map((m) => m.find(({ name }) => name === 'domain').ast.union.DomainObject)
);
}
}

View File

@ -1,2 +0,0 @@
export * from './model';
export * from './meta-builder';

View File

@ -1,43 +0,0 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { IError, ObjectASTNode } from '../jsonc';
import { parse } from '../jsonc/json-parser';
import { applyValue } from './apply-value';
import { ErrorObservable, MetaErrorEmitter } from './meta-error-emitter';
import { MetaStruct, MetaUnion } from './model';
@Injectable()
export class MetaApplicator implements ErrorObservable {
private errorEmitter: MetaErrorEmitter;
constructor() {
this.errorEmitter = new MetaErrorEmitter();
}
get errors(): Observable<string> {
return this.errorEmitter.errors;
}
apply(meta: MetaStruct | MetaUnion, json: string): MetaStruct | MetaUnion | null {
if (!meta || !json) {
this.errorEmitter.emitErrors(['Meta or value is null']);
return null;
}
const document = parse(json);
if (document.errors.length > 0) {
this.emitMonacoErrors(document.errors);
return null;
}
const appliedResult = applyValue(meta, document.root as ObjectASTNode);
if (appliedResult.errors.length > 0) {
this.errorEmitter.emitErrors(appliedResult.errors);
return null;
}
return appliedResult.applied;
}
private emitMonacoErrors(errors: IError[]) {
this.errorEmitter.emitErrors(errors.map((e) => e.message));
}
}

View File

@ -1,39 +0,0 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { DefinitionService } from './definition.service';
import { buildInitialMeta, findMeta, MetaEnricher } from './meta-builder';
import { ErrorObservable, MetaErrorEmitter } from './meta-error-emitter';
import { MetaStruct, MetaUnion } from './model';
@Injectable()
export class MetaBuilder implements ErrorObservable {
private errorEmitter: MetaErrorEmitter;
constructor(private definitionService: DefinitionService) {
this.errorEmitter = new MetaErrorEmitter();
}
get errors(): Observable<string> {
return this.errorEmitter.errors;
}
build(type: string, namespace: string): Observable<MetaStruct | MetaUnion | null> {
return this.definitionService.astDefinition.pipe(
map((astDef) => {
const initial = buildInitialMeta(astDef);
const target = findMeta<MetaStruct | MetaUnion>({ namespace, type }, initial);
if (!target) {
this.errorEmitter.emitErrors(['Target meta not found']);
}
const enricher = new MetaEnricher(namespace, initial);
const { errors, enriched } = enricher.enrich(target);
if (errors.length > 0) {
this.errorEmitter.emitErrors(errors);
}
return errors.length === 0 ? enriched : null;
})
);
}
}

View File

@ -1,90 +0,0 @@
import { Enums, Field, JsonAST, Structs, TypeDefs, Unions } from '@vality/thrift-ts';
import {
ASTDefinition,
MetaEnum,
MetaField,
MetaStruct,
MetaType,
MetaTyped,
MetaTypedef,
MetaUnion,
} from '../model';
import { MetaGroup } from './model';
import { resolveAstValueType } from './resolve-ast-value-type';
import { isRef } from './utils';
const resolveAstField = ({ option, name, type }: Field): MetaField => ({
required: option ? option === 'required' : false,
name,
meta: resolveAstValueType(type),
});
const resolveAstFields = (fields: Field[]): MetaField[] => fields.map((f) => resolveAstField(f));
const resolveAstEnums = (ast: Enums): MetaEnum[] =>
Object.keys(ast).map((name) => ({
type: MetaType.enum,
name,
items: ast[name].items,
}));
const resolveAstStructs = (ast: Structs, namespace: string): MetaStruct[] =>
Object.keys(ast).map((name) => ({
type: MetaType.struct,
name,
fields: resolveAstFields(ast[name]),
isRef: isRef(name),
namespace,
virgin: true,
}));
const resolveAstUnion = (ast: Unions, namespace: string): MetaUnion[] =>
Object.keys(ast).map((name) => ({
type: MetaType.union,
name,
fields: resolveAstFields(ast[name]),
settedField: null,
namespace,
virgin: true,
}));
const resolveAstTypedef = (ast: TypeDefs): MetaTypedef[] =>
Object.keys(ast).map((name) => ({
type: MetaType.typedef,
name,
meta: resolveAstValueType(ast[name].type),
}));
function resolveJsonAst(ast: JsonAST, namespace: string): MetaTyped[] {
let r = [];
if (ast.enum) {
r = [...r, ...resolveAstEnums(ast.enum)];
}
if (ast.struct) {
r = [...r, ...resolveAstStructs(ast.struct, namespace)];
}
if (ast.union) {
r = [...r, ...resolveAstUnion(ast.union, namespace)];
}
if (ast.typedef) {
r = [...r, ...resolveAstTypedef(ast.typedef)];
}
return r;
}
export function buildInitialMeta(astDef: ASTDefinition[]): MetaGroup[] {
if (!astDef || astDef.length === 0) {
return;
}
return astDef.reduce(
(r, { name, ast }) => [
...r,
{
namespace: name,
meta: resolveJsonAst(ast, name),
},
],
[]
);
}

View File

@ -1 +0,0 @@
export * from './meta-enricher';

View File

@ -1,215 +0,0 @@
import {
MetaCollection,
MetaMap,
MetaStruct,
MetaType,
MetaTyped,
MetaTypedef,
MetaTypeDefined,
MetaUnion,
PrimitiveType,
} from '../../model';
import { findMeta } from '../find-meta';
import { MetaGroup, MetaTypeCondition } from '../model';
import { resolvePrimitive } from '../resolve-ast-value-type';
import { isObjectRefType, isPrimitiveType, registerError } from '../utils';
import { MetaLoopResolver } from './meta-loop-resolver';
type MetaLoop = string;
type ObjectRef = string;
export interface EnrichResult {
enriched: MetaStruct | MetaUnion;
errors: string[];
}
export class MetaEnricher {
private objectRefs: MetaTypeDefined[] = [];
private enrichedObjects: (MetaStruct | MetaUnion)[] = [];
private errors: string[] = [];
private hasLoop = false;
private externalNamespaces: string[] = [];
constructor(
private namespace: string,
private shallowMetaDef: MetaGroup[],
private loopSign = '$loop_'
) {}
enrich(target: MetaStruct | MetaUnion): EnrichResult {
if (!target) {
return null;
}
this.registerObjectRef(target);
const enriched = this.enrichStructUnion(target);
return this.preserveLoop(enriched);
}
private preserveLoop(enriched: MetaStruct | MetaUnion): EnrichResult {
if (this.hasLoop) {
const resolver = new MetaLoopResolver(this.enrichedObjects, this.loopSign);
const { resolved, errors } = resolver.resolve(enriched);
return {
enriched: resolved,
errors: [...this.errors, ...errors],
};
}
return { enriched, errors: this.errors };
}
private enrichStructUnion(meta: MetaStruct | MetaUnion): MetaStruct | MetaUnion {
const fields = meta.fields.map((f) => ({
...f,
meta: this.enrichObjectMeta(f.meta),
}));
const result = {
...meta,
fields,
};
this.enrichedObjects = [...this.enrichedObjects, result];
return result;
}
private enrichTyped(meta: MetaTyped): MetaTyped {
switch (meta.type) {
case MetaType.struct:
case MetaType.union:
return this.enrichStructUnion(meta as MetaStruct | MetaUnion);
case MetaType.typedef:
return this.enrichTypedef(meta as MetaTypedef);
case MetaType.collection:
return this.enrichCollection(meta as MetaCollection);
case MetaType.map:
return this.enrichMap(meta as MetaMap);
case MetaType.primitive:
case MetaType.enum:
return meta;
}
this.registerError(`Unsupported enrichment MetaType: ${String(meta.type)}`);
}
private enrichTypedef({ meta }: MetaTypedef): MetaTypedef {
const result = this.enrichObjectMeta(meta);
if (result === this.loopSign) {
this.registerError(`Typedef enrichment should not be looped`);
}
return result as MetaTypedef;
}
private enrichCollection(meta: MetaCollection): MetaCollection {
return {
...meta,
itemMeta: this.enrichCollectionMapMeta(meta.itemMeta),
};
}
private enrichMap(meta: MetaMap): MetaMap {
return {
...meta,
keyMeta: this.enrichCollectionMapMeta(meta.keyMeta),
valueMeta: this.enrichCollectionMapMeta(meta.valueMeta),
};
}
private enrichCollectionMapMeta(
meta: MetaTyped | ObjectRef | PrimitiveType
): MetaTyped | MetaLoop {
if (isObjectRefType(meta)) {
return this.enrichObjectRefWithLoopCheck(meta as ObjectRef);
}
if (isPrimitiveType(meta)) {
return resolvePrimitive(meta as PrimitiveType);
}
return this.enrichTyped(meta as MetaTyped);
}
private enrichObjectMeta(meta: MetaTyped | ObjectRef): MetaTyped | MetaLoop {
if (isObjectRefType(meta)) {
return this.enrichObjectRefWithLoopCheck(meta as ObjectRef);
}
return this.enrichTyped(meta as MetaTyped);
}
private enrichObjectRefWithLoopCheck(meta: ObjectRef): MetaTyped | MetaLoop {
if (this.isObjectRefUsed(meta)) {
this.hasLoop = true;
return `${this.loopSign}${meta}`;
}
return this.enrichObjectRef(this.getCondition(meta));
}
private enrichObjectRef(condition: MetaTypeCondition): MetaTyped | ObjectRef {
const found = this.findMeta(condition);
const conditionState = `${condition.namespace}.${condition.type}`;
if (found === null) {
this.registerError(`Meta not found: ${conditionState}. Bump Control Center damsel!`);
return condition.type;
}
if (!found.type) {
this.registerError(`Meta should be typed: ${conditionState}`);
return condition.type;
}
if (!found.name) {
this.registerError(`Meta should be type defined: ${conditionState}`);
return condition.type;
}
this.registerObjectRef(found);
return this.enrichTyped(found);
}
private findMeta(condition: MetaTypeCondition): (MetaTyped & MetaTypeDefined) | null {
let found = findMeta<MetaTyped & MetaTypeDefined>(condition, this.shallowMetaDef);
if (found) {
return found;
}
for (const usedNamespace of this.externalNamespaces) {
found = findMeta<MetaTyped & MetaTypeDefined>(
{ ...condition, namespace: usedNamespace },
this.shallowMetaDef
);
if (found) {
break;
}
}
return found;
}
private registerError(message: string, prefix = 'Enrichment error'): void {
this.errors = registerError(this.errors, message, prefix);
}
private registerObjectRef(meta: MetaTyped & MetaTypeDefined): void {
switch (meta.type) {
case MetaType.struct:
case MetaType.union:
this.objectRefs = [...this.objectRefs, meta];
}
}
private isObjectRefUsed(meta: ObjectRef): boolean {
return !!this.objectRefs.find(({ name }) => name === meta);
}
private getCondition(meta: ObjectRef): MetaTypeCondition {
const [first, second] = meta.split('.');
if (second) {
this.registerExternalNamespace(first);
}
return second
? {
namespace: first,
type: second,
}
: {
namespace: this.namespace,
type: first,
};
}
private registerExternalNamespace(namespace: string): void {
const found = this.externalNamespaces.find((n) => n === namespace);
if (!found) {
this.externalNamespaces = this.externalNamespaces.concat(namespace);
}
}
}

View File

@ -1,104 +0,0 @@
import cloneDeep from 'lodash-es/cloneDeep';
import { MetaLoopResolver } from './meta-loop-resolver';
describe('MetaLoopResolver', () => {
it('should resolve loop', () => {
const predicate = {
name: 'Predicate',
fields: [
{
required: false,
name: 'all_of',
meta: {
collectionType: 'set',
itemMeta: '$loop_Predicate',
type: 'collection',
},
},
],
settedField: null,
type: 'union',
} as any;
const resolver = new MetaLoopResolver([predicate], '$loop_');
const result = resolver.resolve(predicate);
const expected = cloneDeep(predicate);
expected.fields[0].meta.itemMeta = expected;
expect(result.errors.length).toEqual(0);
expect(result.resolved).toEqual(expected);
});
it('should resolve multi level loops', () => {
const resolveContainer = [
{
name: 'PaymentsProvisionTerms',
fields: [
{
meta: '$loop_CurrencySelector',
name: 'currencies',
required: true,
},
{
meta: '$loop_Predicate',
name: 'predicate',
required: true,
},
],
isRef: false,
type: 'struct',
},
{
name: 'CurrencySelector',
fields: [
{
required: false,
name: 'value',
meta: {
collectionType: 'set',
itemMeta: '$loop_CurrencyRef',
type: 'collection',
},
},
],
settedField: null,
type: 'union',
},
{
name: 'CurrencyRef',
fields: [
{
meta: { type: 'primitive', primitiveType: 'string' },
name: 'symbolic_code',
required: true,
},
],
isRef: true,
type: 'struct',
},
{
name: 'Predicate',
fields: [
{
required: false,
name: 'all_of',
meta: {
collectionType: 'set',
itemMeta: '$loop_Predicate',
type: 'collection',
},
},
],
settedField: null,
type: 'union',
},
] as any;
const resolver = new MetaLoopResolver(resolveContainer, '$loop_');
const result = resolver.resolve(resolveContainer[0]);
// eslint-disable-next-line no-console
console.log('result', result.resolved);
});
});

View File

@ -1,116 +0,0 @@
import {
MetaCollection,
MetaField,
MetaMap,
MetaStruct,
MetaType,
MetaTyped,
MetaUnion,
} from '../../model';
import { registerError } from '../utils';
export interface ResolveLoopResult {
resolved: MetaStruct | MetaUnion;
errors: string[];
}
export class MetaLoopResolver {
private resolved: (MetaStruct | MetaUnion)[] = [];
private errors: string[] = [];
constructor(
private resolveContainer: (MetaStruct | MetaUnion)[],
private loopSign: string,
private deep = 0,
private deepLimit = 3
) {}
resolve(target: MetaStruct | MetaUnion): ResolveLoopResult {
const result = {
resolved: this.resolveObject(target),
errors: this.errors,
};
return result;
}
private resolveMeta(meta: MetaTyped | string): MetaTyped | string {
if (this.isLoop(meta)) {
return this.findResolved(meta as string);
}
const metaTyped = meta as MetaTyped;
switch (metaTyped.type) {
case MetaType.struct:
case MetaType.union:
return this.resolveObject(meta as MetaUnion);
case MetaType.collection:
return this.resolveCollection(meta as MetaCollection);
case MetaType.map:
return this.resolveMap(meta as MetaMap);
case MetaType.typedef:
this.registerError('Unexpected meta type: typedef');
}
return meta;
}
private resolveObject(meta: MetaUnion | MetaStruct): MetaUnion | MetaStruct {
if (this.isResolved(meta)) {
return meta;
}
meta.fields = this.resolveFields(meta.fields);
return meta;
}
private resolveFields(fields: MetaField[]): MetaField[] {
for (const field of fields) {
field.meta = this.resolveMeta(field.meta);
}
return fields;
}
private resolveMap(meta: MetaMap): MetaMap {
meta.keyMeta = this.resolveMeta(meta.keyMeta);
meta.valueMeta = this.resolveMeta(meta.valueMeta);
return meta;
}
private resolveCollection(meta: MetaCollection): MetaCollection {
meta.itemMeta = this.resolveMeta(meta.itemMeta);
return meta;
}
private isLoop(meta: MetaTyped | string): boolean {
return typeof meta === 'string' && meta.startsWith(this.loopSign);
}
private isResolved(meta: MetaUnion | MetaStruct): boolean {
return !!this.resolved.find((i) => i.name === meta.name);
}
private findResolved(looped: string): MetaUnion | MetaStruct {
const found = this.resolveContainer.find(
(i) => i.name === looped.replace(this.loopSign, '')
);
if (!found) {
this.registerError('Resolved meta not found');
}
if (this.deep < this.deepLimit) {
this.deep++;
const resolver = new MetaLoopResolver(
this.resolveContainer,
this.loopSign,
this.deep,
this.deepLimit
);
const { resolved, errors } = resolver.resolve(found);
this.errors = [...this.errors, ...errors];
this.resolved.push(resolved);
} else {
this.resolved.push(found);
}
return found;
}
private registerError(message: string, prefix = 'Resolve loop error'): void {
this.errors = registerError(this.errors, message, prefix);
}
}

View File

@ -1,17 +0,0 @@
import { MetaObject, MetaTyped, MetaTypeDefined } from '../model';
import { MetaGroup, MetaTypeCondition } from './model';
export function findMeta<T extends MetaTypeDefined | MetaTyped | MetaObject>(
condition: MetaTypeCondition,
group: MetaGroup[]
): T | null {
if (!condition || !group) {
return null;
}
const byNamespace = group.find(({ namespace }) => namespace === condition.namespace);
if (!byNamespace) {
return null;
}
const found = byNamespace.meta.find(({ name }) => name === condition.type) as T;
return found ? found : null;
}

View File

@ -1,3 +0,0 @@
export * from './build-initial-meta';
export * from './enrichment/meta-enricher';
export * from './find-meta';

View File

@ -1,11 +0,0 @@
import { MetaTypeDefined } from '../model';
export interface MetaTypeCondition {
type: string;
namespace: string;
}
export interface MetaGroup {
namespace: string;
meta: MetaTypeDefined[];
}

View File

@ -1,52 +0,0 @@
import { ListType, MapType, SetType, ValueType } from '@vality/thrift-ts';
import {
CollectionType,
MetaCollection,
MetaMap,
MetaPrimitive,
MetaType,
PrimitiveType,
} from '../model';
import { isComplexType, isPrimitiveType } from './utils';
const resolveCollection = (
collectionType: CollectionType,
itemType: ValueType
): MetaCollection => ({
type: MetaType.collection,
collectionType,
itemMeta: resolveAstValueType(itemType),
});
const resolveMap = (keyType: ValueType, valueType: ValueType): MetaMap => ({
type: MetaType.map,
keyMeta: resolveAstValueType(keyType),
valueMeta: resolveAstValueType(valueType),
});
export const resolvePrimitive = (primitiveType: PrimitiveType): MetaPrimitive => ({
type: MetaType.primitive,
primitiveType,
});
export function resolveAstValueType(
type: ValueType
): MetaPrimitive | MetaCollection | MetaMap | string {
if (isPrimitiveType(type)) {
return resolvePrimitive(type as PrimitiveType);
}
if (isComplexType(type, 'set')) {
return resolveCollection(CollectionType.set, (type as SetType).valueType);
}
if (isComplexType(type, 'list')) {
return resolveCollection(CollectionType.list, (type as ListType).valueType);
}
if (isComplexType(type, 'map')) {
return resolveMap((type as MapType).keyType, (type as MapType).valueType);
}
if (typeof type === 'string') {
return type;
}
throw Error('Unknown ast value type');
}

View File

@ -1,24 +0,0 @@
import { ValueType } from '@vality/thrift-ts';
import isObject from 'lodash-es/isObject';
import { PrimitiveType } from '../model';
export const isPrimitiveType = (type: any): boolean =>
typeof type === 'string' && Object.keys(PrimitiveType).includes(type);
export const isObjectRefType = (meta: any) => typeof meta === 'string' && !isPrimitiveType(meta);
export const isComplexType = (type: ValueType, typeName: 'map' | 'list' | 'set'): boolean => {
if (!isObject(type)) {
return false;
}
const { name } = type;
return name === typeName;
};
export const isRef = (name: string): boolean => name.endsWith('Ref');
export const registerError = (errContainer: string[], message: string, prefix: string) => {
const error = `${prefix}. ${message}`;
return [...errContainer, error];
};

View File

@ -1,19 +0,0 @@
import { Observable, Subject } from 'rxjs';
export interface ErrorObservable {
errors: Observable<string>;
}
export class MetaErrorEmitter {
private errors$: Subject<string> = new Subject();
get errors(): Observable<string> {
return this.errors$;
}
emitErrors(errors: string[]) {
for (const error of errors) {
this.errors$.next(error);
}
}
}

View File

@ -1,8 +0,0 @@
import { JsonAST } from '@vality/thrift-ts';
// eslint-disable-next-line @typescript-eslint/naming-convention
export interface ASTDefinition {
path: string;
name: string;
ast: JsonAST;
}

View File

@ -1,84 +0,0 @@
export interface MetaTyped {
type: MetaType;
}
export interface MetaObject {
virgin: boolean;
fields: MetaField[];
}
export interface MetaTypeDefined {
namespace: string;
name: string;
}
export interface MetaField {
required: boolean;
name: string;
meta: MetaTyped | string;
}
export interface MetaStruct extends MetaTyped, MetaObject, MetaTypeDefined {
isRef: boolean;
}
export interface MetaUnion extends MetaTyped, MetaObject, MetaTypeDefined {
settedField: string | null;
}
export interface MetaPrimitive extends MetaTyped {
primitiveType: PrimitiveType;
value?: string | number | boolean;
}
export interface MetaEnum extends MetaTyped {
items: { name: string; value: string | number | boolean }[];
value?: number;
}
export interface MetaCollection extends MetaTyped {
collectionType: CollectionType;
itemMeta: MetaTyped | string;
value?: MetaTyped[];
}
export interface MetaMap extends MetaTyped {
keyMeta: MetaTyped | string;
valueMeta: MetaTyped | string;
value?: Map<MetaTyped, MetaTyped>;
}
export interface MetaTypedef extends MetaTyped {
meta: MetaTyped | string;
}
/* eslint-disable @typescript-eslint/naming-convention */
export enum MetaType {
struct = 'struct',
union = 'union',
primitive = 'primitive',
collection = 'collection',
map = 'map',
enum = 'enum',
typedef = 'typedef',
}
export enum PrimitiveType {
string = 'string',
i8 = 'i8',
i16 = 'i16',
i32 = 'i32',
i64 = 'i64',
bool = 'bool',
int = 'int',
double = 'double',
binary = 'binary',
}
export enum CollectionType {
set = 'set',
list = 'list',
}
/* eslint-enable @typescript-eslint/naming-convention */
export * from './ast-definition';

View File

@ -1,29 +0,0 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ErrorObservable, MetaErrorEmitter } from './meta-error-emitter';
import { MetaStruct, MetaUnion } from './model';
import { buildStructUnion, ThriftType } from './thrift-builder';
@Injectable()
export class ThriftBuilderService implements ErrorObservable {
private errorEmitter: MetaErrorEmitter;
constructor() {
this.errorEmitter = new MetaErrorEmitter();
}
get errors(): Observable<string> {
return this.errorEmitter.errors;
}
build(meta: MetaStruct | MetaUnion): ThriftType | null {
try {
return buildStructUnion(meta);
} catch (ex) {
console.error(ex);
this.errorEmitter.emitErrors([ex.message]);
return null;
}
}
}

View File

@ -1,66 +0,0 @@
import { getThriftInstance } from '../../thrift-services';
import {
MetaCollection,
MetaEnum,
MetaMap,
MetaPrimitive,
MetaStruct,
MetaType,
MetaTyped,
MetaUnion,
} from '../model';
export type ThriftType = any;
const buildPrimitive = ({ value }: MetaPrimitive): string | number | boolean => value;
const buildEnum = ({ value }: MetaEnum): number => value;
const buildCollection = ({ value }: MetaCollection): ThriftType[] => {
if (!value) {
return;
}
return value.map(buildTyped);
};
function buildMap({ value }: MetaMap): Map<ThriftType, ThriftType> {
if (!value) {
return;
}
const result = new Map();
value.forEach((v, k) => result.set(buildTyped(k), buildTyped(v)));
return result;
}
function buildTyped(meta: MetaTyped): ThriftType {
switch (meta.type) {
case MetaType.struct:
case MetaType.union:
return buildStructUnion(meta as MetaStruct);
case MetaType.primitive:
return buildPrimitive(meta as MetaPrimitive);
case MetaType.enum:
return buildEnum(meta as MetaEnum);
case MetaType.collection:
return buildCollection(meta as MetaCollection);
case MetaType.map:
return buildMap(meta as MetaMap);
}
throw new Error(`Unsupported meta type: ${meta.type}`);
}
export function buildStructUnion({
namespace,
name,
fields,
virgin,
}: MetaStruct | MetaUnion): ThriftType {
if (virgin) {
return;
}
const instance = getThriftInstance(namespace, name);
for (const field of fields) {
instance[field.name] = buildTyped(field.meta as MetaTyped);
}
return instance;
}

View File

@ -1 +0,0 @@
export * from './build-thrift';

View File

@ -1,23 +0,0 @@
import { Injectable } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import { Subject } from 'rxjs';
@Injectable()
export class DetailsContainerService {
opened$: Subject<boolean> = new Subject();
private detailsContainer: MatSidenav;
set container(sidenav: MatSidenav) {
this.detailsContainer = sidenav;
this.detailsContainer.openedChange.subscribe((opened) => this.opened$.next(opened));
}
open() {
void this.detailsContainer.open();
}
close() {
void this.detailsContainer.close();
}
}

View File

@ -1,13 +0,0 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { DomainPair } from './domain-group';
@Injectable()
export class DomainDetailsService {
domainPair$: Subject<DomainPair> = new Subject();
emit(p: DomainPair) {
this.domainPair$.next(p);
}
}

View File

@ -1,4 +1,61 @@
<mat-card>
<mat-card-subtitle>Snapshot version: {{ version }} </mat-card-subtitle>
<mat-card-content> <cc-group-table [group]="group"></cc-group-table> </mat-card-content>
</mat-card>
<div>
<div gdColumns="1fr 1fr" gdGap="24px">
<cc-select
[formControl]="typesControl"
[options]="options$ | async"
label="Object types"
multiple
></cc-select>
<mat-form-field>
<mat-label>RegExp patter</mat-label>
<input [formControl]="searchControl" matInput />
</mat-form-field>
</div>
<mat-card>
<mat-card-content>
<div class="table-wrapper">
<table
[dataSource]="dataSource$ | async"
mat-table
matSort
matSortActive="ref"
matSortDirection="asc"
matSortDisableClear
>
<ng-container [matColumnDef]="cols.def.type" sticky>
<th *matHeaderCellDef mat-header-cell mat-sort-header>Type</th>
<td *matCellDef="let c" mat-cell>{{ c.type }}</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.ref">
<th *matHeaderCellDef mat-header-cell mat-sort-header>Ref</th>
<td *matCellDef="let c" class="json-cell" mat-cell>
<cc-pretty-json [object]="c.ref | ccUnionValue" inline></cc-pretty-json>
</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.obj">
<th *matHeaderCellDef mat-header-cell mat-sort-header>Object</th>
<td *matCellDef="let c" class="json-cell" mat-cell>
<cc-pretty-json
[object]="(c.obj | ccUnionValue)?.data"
inline
></cc-pretty-json>
</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.actions" stickyEnd>
<th *matHeaderCellDef mat-header-cell></th>
<td *matCellDef="let c" mat-cell style="width: 0">
<button mat-icon-button (click)="openDetails(c)">
<mat-icon>visibility</mat-icon>
</button>
</td>
</ng-container>
<cc-no-data-row></cc-no-data-row>
<tr *matHeaderRowDef="cols.list; sticky: true" mat-header-row></tr>
<tr *matRowDef="let row; columns: cols.list" mat-row></tr>
</table>
</div>
<mat-paginator [pageSizeOptions]="[25, 100]" showFirstLastButtons></mat-paginator>
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,41 @@
$offset: 16px;
.table-wrapper {
$header-height: 64px;
$title-height: 40px;
$offsets: $offset * 4;
$internal-offsets: 16px * 2;
$paginator-height: 63.555px;
$filters-height: 57.555px;
overflow: scroll;
height: calc(
100vh -
(
$header-height + $title-height + $offsets + $internal-offsets + $paginator-height +
$filters-height
)
);
table {
width: 100%;
td,
th {
padding: 0 4px;
}
.json-cell {
overflow: hidden;
white-space: nowrap;
}
}
.mat-table-sticky-border-elem-right {
border-left: 1px solid #e0e0e0;
}
.mat-table-sticky-border-elem-left {
border-right: 1px solid #e0e0e0;
}
}

View File

@ -1,33 +1,112 @@
import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { filter } from 'rxjs/operators';
import { Component, ViewChildren, QueryList, Output, EventEmitter, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { UntilDestroy } from '@ngneat/until-destroy';
import { Reference, DomainObject } from '@vality/domain-proto/lib/domain';
import sortBy from 'lodash-es/sortBy';
import { combineLatest, Observable } from 'rxjs';
import { map, switchMap, startWith, shareReplay } from 'rxjs/operators';
import { DomainGroup } from './domain-group';
import { DomainGroupService } from './domain-group.service';
import { Columns } from '../../../../components/table';
import { objectToJSON } from '../../../api/utils';
import { QueryParamsService } from '../../../shared/services';
import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service';
import { MetadataService } from '../../services/metadata.service';
import { DataSourceItem } from './types/data-source-item';
import { filterPredicate } from './utils/filter-predicate';
import { sortData } from './utils/sort-table-data';
interface Params {
types?: string[];
}
@UntilDestroy()
@Component({
selector: 'cc-domain-group',
templateUrl: './domain-group.component.html',
providers: [DomainGroupService],
styleUrls: ['./domain-group.component.scss'],
})
export class DomainGroupComponent implements OnInit {
group: DomainGroup[];
version: number;
@Output() refChange = new EventEmitter<{ ref: Reference; obj: DomainObject }>();
constructor(private groupService: DomainGroupService, private snackBar: MatSnackBar) {}
@ViewChildren(MatPaginator) paginator = new QueryList<MatPaginator>();
@ViewChildren(MatSort) sort = new QueryList<MatSort>();
searchControl = new FormControl('');
typesControl = new FormControl(this.queryParamsService.params.types || []);
dataSource$: Observable<MatTableDataSource<DataSourceItem>> =
this.domainStoreService.domain$.pipe(
map((domain) => Array.from(domain).map(([ref, obj]) => ({ ref, obj }))),
switchMap((data) =>
combineLatest(
data.map((d) => this.metadataService.getDomainObjectType(d.ref))
).pipe(
map((r) =>
r.map((type, idx) => ({
...data[idx],
type,
stringified: JSON.stringify(
objectToJSON([data[idx].obj, data[idx].ref, type])
),
}))
)
)
),
switchMap((data: DataSourceItem[]) =>
combineLatest([
this.searchControl.valueChanges.pipe(startWith(this.searchControl.value)),
this.typesControl.valueChanges.pipe(startWith(this.typesControl.value)),
this.paginator.changes.pipe(startWith(this.paginator)),
this.sort.changes.pipe(startWith(this.sort)),
]).pipe(
map(([searchStr, selectedTypes]) =>
this.createMatTableDataSource(data, searchStr, selectedTypes)
)
)
),
shareReplay({ refCount: true, bufferSize: 1 })
);
cols = new Columns('type', 'ref', 'obj', 'actions');
fields$ = this.metadataService.getDomainFields().pipe(
map((fields) => sortBy(fields, 'type')),
shareReplay({ refCount: true, bufferSize: 1 })
);
options$ = this.fields$.pipe(
map((fields) => fields.map(({ type }) => ({ label: type, value: type })))
);
isLoading$ = this.domainStoreService.isLoading$;
constructor(
private domainStoreService: DomainStoreService,
private metadataService: MetadataService,
private queryParamsService: QueryParamsService<Params>
) {}
ngOnInit() {
this.groupService.initialize().subscribe(({ group, version }) => {
this.group = group;
this.version = version;
this.typesControl.valueChanges.subscribe((types) => {
void this.queryParamsService.set({ types });
});
this.groupService.undefDetectionStatus$
.pipe(filter((s) => s === 'detected'))
.subscribe(() =>
this.snackBar.open(
'Detected undefined domain types. Need to bump damsel version.',
'OK'
)
);
}
openDetails(item: DataSourceItem) {
this.refChange.emit({ ref: item.ref, obj: item.obj });
}
private createMatTableDataSource(
data: DataSourceItem[],
searchStr: string,
selectedTypes: string[]
) {
const dataSource = new MatTableDataSource(
data.filter((d) => selectedTypes.includes(d.type))
);
dataSource.paginator = this.paginator?.first;
dataSource.sort = this.sort?.first;
dataSource.sortData = sortData;
dataSource.filterPredicate = filterPredicate;
dataSource.filter = searchStr.trim();
return dataSource;
}
}

View File

@ -2,7 +2,7 @@ import { CdkTableModule } from '@angular/cdk/table';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
@ -10,24 +10,21 @@ import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { PipesModule } from '@vality/ng-core';
import { PrettyJsonModule } from '@cc/components/pretty-json';
import { TableModule } from '../../../../components/table';
import { ThriftPipesModule } from '../../../shared';
import { SelectModule } from '../../../shared/components/select';
import { DomainGroupComponent } from './domain-group.component';
import { DomainObjectsTypeSelectorComponent } from './domain-objects-type-selector';
import { GroupControlComponent } from './group-control';
import { GroupTableComponent } from './group-table';
@NgModule({
declarations: [
DomainGroupComponent,
DomainObjectsTypeSelectorComponent,
GroupControlComponent,
GroupTableComponent,
],
declarations: [DomainGroupComponent],
imports: [
CommonModule,
FormsModule,
@ -44,6 +41,12 @@ import { GroupTableComponent } from './group-table';
MatSelectModule,
MatSortModule,
PrettyJsonModule,
PipesModule,
TableModule,
ReactiveFormsModule,
SelectModule,
MatProgressSpinnerModule,
ThriftPipesModule,
],
exports: [DomainGroupComponent],
})

View File

@ -1,40 +0,0 @@
import { Injectable } from '@angular/core';
import { Int64 } from '@vality/thrift-ts';
import { AsyncSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { DomainInfoService } from '../domain-info.service';
import { DomainGroup } from './domain-group';
import { group } from './group-domain-objects';
@Injectable()
export class DomainGroupService {
undefDetectionStatus$ = new AsyncSubject();
constructor(private domainInfoService: DomainInfoService) {}
initialize(): Observable<{ version: number; group: DomainGroup[] }> {
return this.domainInfoService.payload$.pipe(
map(({ shapshot: { version, domain }, domainDef }) => {
const domainGroup = group(domain, domainDef);
this.detectUndefGroup(domainGroup);
return {
version: (version as unknown as Int64).toNumber(),
group: this.filterUndef(domainGroup),
};
})
);
}
private detectUndefGroup(domainGroup: DomainGroup[]) {
const undef = domainGroup.find((g) => g.name === 'undef');
if (undef) {
this.undefDetectionStatus$.next('detected');
}
this.undefDetectionStatus$.complete();
}
private filterUndef(domainGroup: DomainGroup[]) {
return domainGroup.filter((g) => g.name !== 'undef');
}
}

View File

@ -1,16 +0,0 @@
import { Reference } from '@vality/domain-proto/lib/domain_config';
export interface AbstractDomainObject {
ref: any;
data: any;
}
export interface DomainPair {
ref: Reference;
object: AbstractDomainObject;
}
export interface DomainGroup {
name: string | 'undef';
pairs?: DomainPair[];
}

View File

@ -1,17 +0,0 @@
<div fxLayout>
<mat-form-field fxFlex>
<mat-select
#nameSelect="ngModel"
[(ngModel)]="selectedNames"
multiple
placeholder="Domain objects"
(selectionChange)="selectionChange($event)"
>
<mat-option class="filter-option" disabled="disabled">
<button mat-button (click)="selectAll(nameSelect)">SELECT ALL</button>
<button mat-button (click)="deselectAll(nameSelect)">DESELECT ALL</button>
</mat-option>
<mat-option *ngFor="let name of names" [value]="name"> {{ name }} </mat-option>
</mat-select>
</mat-form-field>
</div>

View File

@ -1,37 +0,0 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { NgModel } from '@angular/forms';
import { MatSelectChange } from '@angular/material/select';
import { DomainGroup } from '../domain-group';
@Component({
selector: 'cc-domain-objects-type-selector',
templateUrl: './domain-objects-type-selector.component.html',
})
export class DomainObjectsTypeSelectorComponent implements OnChanges {
@Input() group: DomainGroup[];
@Output() typeSelectionChange: EventEmitter<string[]> = new EventEmitter();
names: string[];
selectedNames: string[];
ngOnChanges({ group }: SimpleChanges) {
if (group && group.currentValue) {
this.names = group.currentValue.map(({ name }) => name);
}
}
selectAll(select: NgModel) {
select.update.emit(this.names);
this.typeSelectionChange.emit(this.names);
}
deselectAll(select: NgModel) {
select.update.emit([]);
this.typeSelectionChange.emit([]);
}
selectionChange(change: MatSelectChange) {
this.typeSelectionChange.emit(change.value);
}
}

View File

@ -1 +0,0 @@
export * from './domain-objects-type-selector.component';

View File

@ -1,27 +0,0 @@
<form
fxLayout
fxLayout.sm="column"
fxLayout.xs="column"
fxLayoutGap="20px"
fxLayoutGap.sm="0"
fxLayoutGap.xs="0"
>
<cc-domain-objects-type-selector
[group]="group"
fxFlex="50"
(typeSelectionChange)="selectionChange($event)"
></cc-domain-objects-type-selector>
<mat-form-field fxFlex="50">
<span matPrefix>/</span>
<input
[value]="pattern"
matInput
placeholder="RegExp pattern"
(keyup)="patternChange($event.target.value)"
/>
<span matSuffix>/</span>
<button aria-label="Clear" mat-button mat-icon-button matSuffix (click)="clearPattern()">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</form>

View File

@ -1,29 +0,0 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DomainGroup } from '../domain-group';
@Component({
selector: 'cc-group-control',
templateUrl: './group-control.component.html',
})
export class GroupControlComponent {
@Input() group: DomainGroup[];
@Output() typeSelectionChange: EventEmitter<string[]> = new EventEmitter();
@Output() regExpPatternChange: EventEmitter<string> = new EventEmitter();
pattern = '';
selectionChange(selectedTypes: string[]) {
this.typeSelectionChange.emit(selectedTypes);
}
patternChange(pattern: string) {
this.pattern = pattern;
this.regExpPatternChange.emit(this.pattern);
}
clearPattern() {
this.pattern = '';
this.regExpPatternChange.emit(this.pattern);
}
}

View File

@ -1 +0,0 @@
export * from './group-control.component';

View File

@ -1,13 +0,0 @@
import { TableDataSource } from './model';
export function filterPredicate({ stringified }: TableDataSource, filter: string): boolean {
let regexp;
try {
regexp = new RegExp(filter, 'g');
} catch {
return false;
}
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
const matched = stringified.match(regexp);
return matched && matched.length > 0;
}

View File

@ -1,40 +0,0 @@
<cc-group-control
[group]="group"
(regExpPatternChange)="applyFilter($event)"
(typeSelectionChange)="setTableData($event)"
></cc-group-control>
<div class="table-container">
<table [dataSource]="dataSource" mat-table matSort>
<ng-container matColumnDef="name" sticky>
<th *matHeaderCellDef mat-header-cell mat-sort-header>Name</th>
<td *matCellDef="let object" mat-cell>{{ object.name }}</td>
</ng-container>
<ng-container matColumnDef="ref">
<th *matHeaderCellDef mat-header-cell mat-sort-header>Ref</th>
<td *matCellDef="let object" mat-cell>
<cc-pretty-json [object]="object.ref" inline="true"></cc-pretty-json>
</td>
</ng-container>
<ng-container matColumnDef="data">
<th *matHeaderCellDef mat-header-cell mat-sort-header>Data</th>
<td *matCellDef="let object" mat-cell>
<cc-pretty-json [object]="object.data" inline="true"></cc-pretty-json>
</td>
</ng-container>
<ng-container matColumnDef="details" stickyEnd>
<th *matHeaderCellDef mat-header-cell>Details</th>
<td *matCellDef="let object; let index = index" mat-cell>
<button mat-icon-button (click)="openDetails(object, index)">
<mat-icon>visibility</mat-icon>
</button>
</td>
</ng-container>
<tr *matHeaderRowDef="cols" mat-header-row></tr>
<tr
*matRowDef="let object; columns: cols; let index = index"
[class.selected-row]="selectedIndex === index && detailsOpened"
mat-row
></tr>
</table>
</div>
<mat-paginator [pageSizeOptions]="[10, 20, 50, 100]" showFirstLastButtons></mat-paginator>

View File

@ -1,45 +0,0 @@
table {
width: 100%;
}
.mat-header-row {
width: 100%;
}
.mat-row {
width: 100%;
}
.mat-column-type {
padding-right: 8px;
}
.mat-column-ref {
padding-left: 8px;
min-width: 100px;
}
.mat-column-data {
white-space: nowrap;
}
.mat-column-details {
padding-left: 8px;
}
.mat-table-sticky:first-child {
border-right: 1px solid #e0e0e0;
}
.mat-table-sticky:last-child {
border-left: 1px solid #e0e0e0;
}
.table-container {
width: 100%;
overflow: auto;
}
.selected-row {
background: #f5f5f5;
}

View File

@ -1,62 +0,0 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { DetailsContainerService } from '../../details-container.service';
import { DomainDetailsService } from '../../domain-details.service';
import { DomainGroup } from '../domain-group';
import { filterPredicate } from './filter-predicate';
import { TableDataSource, TableGroup } from './model';
import { sortData } from './sort-table-data';
import { toDataSource, toTableGroup } from './table-group';
@Component({
selector: 'cc-group-table',
templateUrl: './group-table.component.html',
styleUrls: ['./group-table.component.scss'],
})
export class GroupTableComponent implements OnInit, OnChanges {
@Input() group: DomainGroup[];
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
dataSource: MatTableDataSource<TableDataSource> = new MatTableDataSource();
cols = ['name', 'ref', 'data', 'details'];
selectedIndex: number;
detailsOpened: boolean;
private tableGroup: TableGroup[];
constructor(
private detailsService: DomainDetailsService,
private detailsContainerService: DetailsContainerService
) {}
ngOnChanges({ group }: SimpleChanges) {
if (group && group.currentValue) {
this.tableGroup = toTableGroup(group.currentValue);
}
}
ngOnInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.dataSource.filterPredicate = filterPredicate;
this.dataSource.sortData = sortData;
this.detailsContainerService.opened$.subscribe((opened) => (this.detailsOpened = opened));
}
openDetails({ pair }: TableDataSource, index: number) {
this.selectedIndex = index;
this.detailsService.emit(pair);
}
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim();
}
setTableData(selectedTypes: string[]) {
this.dataSource.data = toDataSource(this.tableGroup, selectedTypes);
}
}

View File

@ -1 +0,0 @@
export * from './group-table.component';

View File

@ -1,25 +0,0 @@
import { DomainPair } from '../domain-group';
interface ViewDomainObject {
ref: string;
data: string;
}
interface TableItem {
stringified: string;
pair: DomainPair;
view: ViewDomainObject;
}
export interface TableGroup {
name: string;
tableItems: TableItem[];
}
export interface TableDataSource {
name: string;
ref: string;
data: string;
pair: DomainPair;
stringified: string;
}

View File

@ -1,82 +0,0 @@
import { MatSort } from '@angular/material/sort';
import isNumber from 'lodash-es/isNumber';
import { TableDataSource } from './model';
function strAsc(a: string, b: string): number {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
}
function strDes(a: string, b: string): number {
if (a > b) {
return -1;
} else if (a < b) {
return 1;
} else {
return 0;
}
}
function numberAsc(a: number, b: number): number {
return a - b;
}
function numberDes(a: number, b: number): number {
return b - a;
}
function sortByStrField(
fieldName: string,
data: TableDataSource[],
sort: MatSort
): TableDataSource[] {
if (sort.direction === 'asc') {
return data.sort((a, b) => strAsc(a[fieldName], b[fieldName]));
}
if (sort.direction === 'desc') {
return data.sort((a, b) => strDes(a[fieldName], b[fieldName]));
}
return data;
}
function sortByRef(data: TableDataSource[], sort: MatSort) {
if (sort.direction === 'asc') {
return data.sort((a, b) => {
if (isNumber(a.pair.object.ref.id)) {
return numberAsc(a.pair.object.ref.id, b.pair.object.ref.id);
}
return strAsc(a.ref, b.ref);
});
}
if (sort.direction === 'desc') {
return data.sort((a, b) => {
if (isNumber(a.pair.object.ref.id)) {
return numberDes(a.pair.object.ref.id, b.pair.object.ref.id);
}
return strDes(a.ref, b.ref);
});
}
return data;
}
export function sortData(data: TableDataSource[], sort: MatSort): TableDataSource[] {
if (!sort.active) {
return data;
}
if (sort.active === 'data') {
return sortByStrField('data', data, sort);
}
if (sort.active === 'name') {
return sortByStrField('name', data, sort);
}
if (sort.active === 'ref') {
return sortByRef(data, sort);
}
return data;
}

View File

@ -1,43 +0,0 @@
import { toJson } from '@cc/utils/thrift-json-converter';
import { DomainGroup } from '../domain-group';
import { TableDataSource, TableGroup } from './model';
function shorten(str: string, limit = 150): string {
return str.length > limit ? str.slice(0, limit) + '...' : str;
}
export function toTableGroup(domainGroup: DomainGroup[]): TableGroup[] {
return domainGroup.map(({ name, pairs }) => ({
name,
tableItems: pairs.map((p) => {
const pair = toJson(p);
const stringifiedRef = JSON.stringify(pair.object.ref);
const stringifiedData = JSON.stringify(pair.object.data);
const stringified = stringifiedRef + stringifiedData;
const view = {
ref: shorten(stringifiedRef),
data: shorten(stringifiedData),
};
return { stringified, pair, view };
}),
}));
}
export function toDataSource(group: TableGroup[], selectedTypes: string[]): TableDataSource[] {
return group
.filter(({ name }) => selectedTypes.includes(name))
.reduce(
(acc, { name, tableItems }) =>
acc.concat(
tableItems.map(({ pair, view: { ref, data }, stringified }) => ({
name,
ref,
data,
pair,
stringified,
}))
),
[]
);
}

View File

@ -1,3 +1,2 @@
export * from './domain-group.module';
export * from './domain-group.component';
export * from './domain-group';

View File

@ -0,0 +1,8 @@
import { Reference, DomainObject } from '@vality/domain-proto/lib/domain';
export interface DataSourceItem {
type: string;
ref: Reference;
obj: DomainObject;
stringified: string;
}

View File

@ -0,0 +1,12 @@
import { DataSourceItem } from '../types/data-source-item';
export function filterPredicate({ stringified }: DataSourceItem, filter: string): boolean {
let regexp;
try {
regexp = new RegExp(filter, 'g');
} catch {
return false;
}
const matched = stringified.match(regexp);
return matched && matched.length > 0;
}

View File

@ -0,0 +1,27 @@
import { MatSort } from '@angular/material/sort';
import sortBy from 'lodash-es/sortBy';
import { objectToJSON } from '../../../../api/utils';
import { DataSourceItem } from '../types/data-source-item';
export function sortData(data: DataSourceItem[], sort: MatSort): DataSourceItem[] {
switch (sort.active as keyof DataSourceItem) {
case 'type':
data = sortBy(data, 'type');
break;
case 'obj':
data = sortBy(data, [(o) => JSON.stringify(objectToJSON(o.obj))]);
break;
case 'ref':
data = sortBy(data, [
(o) => {
const id = o.ref?.['id'];
if (typeof id === 'number') return id;
return JSON.stringify(objectToJSON(o.ref));
},
]);
break;
}
if (sort.direction === 'desc') return data.reverse();
return data;
}

View File

@ -1,25 +1,44 @@
<mat-sidenav-container>
<mat-sidenav
#domainObjDetails
[opened]="!!objWithRef"
fixedInViewport="true"
fixedTopGap="64"
mode="side"
position="end"
>
<div class="details-container" fxLayout="column" fxLayoutGap="10px">
<cc-domain-obj-details fxFlex="95"></cc-domain-obj-details>
<div fxLayout fxLayoutAlign="space-between center">
<button mat-button (click)="closeDetails()">CLOSE</button>
<button mat-button (click)="editDomainObj()">EDIT</button>
</div>
<div class="details-container" fxLayout="column" fxLayoutGap="24px">
<cc-thrift-viewer
[kind]="kind"
[value]="objWithRef?.obj"
class="viewer"
(changeKind)="kind = $event"
></cc-thrift-viewer>
<cc-actions>
<button mat-button (click)="objWithRef = null">CLOSE</button>
<button color="warn" mat-button (click)="delete()">DELETE</button>
<button color="primary" mat-button (click)="edit()">EDIT</button>
</cc-actions>
</div>
</mat-sidenav>
<mat-sidenav-content>
<cc-card-container>
<div *ngIf="isLoading" fxLayout fxLayoutAlign="center stretch">
<div fxLayout="column" fxLayoutGap="24px" style="margin: 24px 16px 4px">
<div fxLayout="row" fxLayoutAlign="space-between">
<h1 class="cc-display-1">
Domain config <span class="cc-secondary-text">#{{ version$ | async }}</span>
</h1>
<div>
<button color="primary" mat-raised-button routerLink="/domain/create">
CREATE OBJECT
</button>
</div>
</div>
<div *ngIf="progress$ | async; else content" fxLayout fxLayoutAlign="center stretch">
<mat-spinner></mat-spinner>
</div>
<cc-domain-group *ngIf="initialized"></cc-domain-group>
</cc-card-container>
<ng-template #content>
<cc-domain-group (refChange)="objWithRef = $event"></cc-domain-group>
</ng-template>
</div>
</mat-sidenav-content>
</mat-sidenav-container>

View File

@ -5,4 +5,9 @@ mat-sidenav {
.details-container {
padding: 20px;
height: 100%;
.viewer {
height: 100%;
overflow: scroll;
}
}

View File

@ -1,63 +1,76 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Component, ViewChild } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { DomainObject, Reference } from '@vality/domain-proto/lib/domain';
import { BaseDialogService, BaseDialogResponseStatus } from '@vality/ng-core';
import { filter, switchMap } from 'rxjs/operators';
import { DetailsContainerService } from './details-container.service';
import { DomainDetailsService } from './domain-details.service';
import { DomainInfoService } from './domain-info.service';
import { ConfirmActionDialogComponent } from '../../../components/confirm-action-dialog';
import { enumHasValue } from '../../../utils';
import { ViewerKind } from '../../shared/components/thrift-viewer';
import { ErrorService } from '../../shared/services/error';
import { NotificationService } from '../../shared/services/notification';
import { DomainStoreService } from '../../thrift-services/damsel/domain-store.service';
const VIEWER_KIND = 'domain-info-kind';
@UntilDestroy()
@Component({
templateUrl: './domain-info.component.html',
styleUrls: ['./domain-info.component.scss'],
providers: [DomainInfoService, DomainDetailsService, DetailsContainerService],
})
export class DomainInfoComponent implements OnInit {
initialized = false;
isLoading: boolean;
export class DomainInfoComponent {
@ViewChild('domainObjDetails', { static: true }) detailsContainer: MatSidenav;
private detailedObjRef: any;
version$ = this.domainStoreService.version$;
progress$ = this.domainStoreService.isLoading$;
objWithRef: { obj: DomainObject; ref: Reference } = null;
get kind() {
const kind = localStorage.getItem(VIEWER_KIND);
if (!enumHasValue(ViewerKind, kind)) {
this.kind = ViewerKind.Editor;
return ViewerKind.Editor;
}
return kind;
}
set kind(kind: ViewerKind) {
localStorage.setItem(VIEWER_KIND, kind);
}
constructor(
private snackBar: MatSnackBar,
private detailsService: DomainDetailsService,
private detailsContainerService: DetailsContainerService,
private domainInfoService: DomainInfoService,
private router: Router
private router: Router,
private domainStoreService: DomainStoreService,
private baseDialogService: BaseDialogService,
private notificationService: NotificationService,
private errorService: ErrorService
) {}
ngOnInit() {
this.initialize();
this.detailsContainerService.container = this.detailsContainer;
this.detailsService.domainPair$.subscribe(({ ref }) => {
this.detailedObjRef = ref;
this.detailsContainerService.open();
});
edit() {
void this.router.navigate(['domain', 'edit', JSON.stringify(this.objWithRef.ref)]);
}
closeDetails() {
this.detailsContainerService.close();
}
editDomainObj() {
void this.router.navigate(['domain', JSON.stringify(this.detailedObjRef)]);
}
private initialize() {
this.isLoading = true;
this.domainInfoService.initialize().subscribe(
() => {
this.isLoading = false;
this.initialized = true;
},
(err) => {
this.isLoading = false;
this.snackBar
.open(`An error occurred while initializing: ${String(err)}`, 'RETRY')
.onAction()
.subscribe(() => this.initialize());
}
);
delete() {
this.baseDialogService
.open(ConfirmActionDialogComponent, { title: 'Delete object' })
.afterClosed()
.pipe(
untilDestroyed(this),
filter(({ status }) => status === BaseDialogResponseStatus.Success),
switchMap(() =>
this.domainStoreService.commit({
ops: [{ remove: { object: this.objWithRef.obj } }],
})
)
)
.subscribe({
next: () => {
this.notificationService.success('Successfully removed');
},
error: (err) => {
this.errorService.error(err);
},
});
}
}

View File

@ -8,17 +8,17 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { CardContainerModule } from '@cc/components/card-container/card-container.module';
import { RouterModule } from '@angular/router';
import { PipesModule, ActionsModule } from '@vality/ng-core';
import { MonacoEditorModule } from '../../monaco-editor';
import { ThriftViewerModule } from '../../shared/components/thrift-viewer';
import { DamselModule } from '../../thrift-services/damsel';
import { DomainGroupModule } from './domain-group';
import { DomainInfoComponent } from './domain-info.component';
import { DomainObjDetailsComponent } from './domain-obj-details';
@NgModule({
declarations: [DomainInfoComponent, DomainObjDetailsComponent],
declarations: [DomainInfoComponent],
imports: [
CommonModule,
DomainGroupModule,
@ -32,7 +32,10 @@ import { DomainObjDetailsComponent } from './domain-obj-details';
MatProgressSpinnerModule,
MonacoEditorModule,
DamselModule,
CardContainerModule,
ThriftViewerModule,
PipesModule,
RouterModule,
ActionsModule,
],
})
export class DomainInfoModule {}

View File

@ -1,33 +0,0 @@
import { Injectable } from '@angular/core';
import { Snapshot } from '@vality/domain-proto/lib/domain_config';
import { Field } from '@vality/thrift-ts';
import { AsyncSubject, combineLatest, Observable, Subject } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { DomainService } from '../domain.service';
import { MetadataService } from '../metadata.service';
export interface Payload {
shapshot: Snapshot;
domainDef: Field[];
}
@Injectable()
export class DomainInfoService {
payload$: Subject<Payload> = new AsyncSubject();
constructor(private domainService: DomainService, private metadataService: MetadataService) {}
initialize(): Observable<void> {
return combineLatest([
this.domainService.shapshot,
this.metadataService.getDomainDef(),
]).pipe(
tap(([shapshot, domainDef]) => {
this.payload$.next({ shapshot, domainDef });
this.payload$.complete();
}),
map(() => null)
);
}
}

View File

@ -1 +0,0 @@
<cc-monaco-editor [file]="file$ | async" [options]="options"></cc-monaco-editor>

View File

@ -1,4 +0,0 @@
cc-monaco-editor {
display: block;
height: 100%;
}

View File

@ -1,30 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { IEditorOptions, MonacoFile } from '../../../monaco-editor';
import { DomainDetailsService } from '../domain-details.service';
@Component({
selector: 'cc-domain-obj-details',
templateUrl: './domain-obj-details.component.html',
styleUrls: ['./domain-obj-details.component.scss'],
})
export class DomainObjDetailsComponent implements OnInit {
file$: Observable<MonacoFile>;
options: IEditorOptions = {
readOnly: true,
};
constructor(private detailsService: DomainDetailsService) {}
ngOnInit() {
this.file$ = this.detailsService.domainPair$.pipe(
map(({ object }) => ({
uri: 'index.json',
language: 'json',
content: JSON.stringify(object, null, 2),
}))
);
}
}

View File

@ -1 +0,0 @@
export * from './domain-obj-details.component';

View File

@ -1,16 +0,0 @@
import { DomainObject, Reference } from '@vality/domain-proto/lib/domain';
import { MetaStruct, MetaUnion } from '../damsel-meta';
export interface ModificationItem {
monacoContent: string;
meta: MetaStruct | MetaUnion;
domainObj: DomainObject;
}
export interface DomainModificationModel {
ref: Reference;
objectType: string;
original: ModificationItem;
modified: Partial<ModificationItem>;
}

View File

@ -0,0 +1,57 @@
<div class="editor-container">
<div fxLayout="column" fxLayoutGap="24px">
<div class="cc-display-1">Create</div>
<mat-card>
<mat-card-content class="content">
<cc-thrift-editor
*ngIf="!review"
[extensions]="extensions$ | async"
[formControl]="control"
[kind]="kind"
[metadata]="metadata$ | async"
class="editor"
namespace="domain"
type="DomainObject"
(changeKind)="kind = $event"
></cc-thrift-editor>
<cc-thrift-viewer
*ngIf="review"
[kind]="reviewKind"
[value]="control.value"
class="editor"
(changeKind)="reviewKind = $event"
></cc-thrift-viewer>
</mat-card-content>
</mat-card>
<cc-actions>
<button
*ngIf="!review"
[disabled]="!!(progress$ | async)"
mat-button
routerLink="/domain"
>
<mat-icon aria-label="Login">keyboard_arrow_left</mat-icon>
BACK TO DOMAIN
</button>
<button
*ngIf="review"
[disabled]="!!(progress$ | async)"
mat-button
(click)="review = false"
>
<mat-icon aria-label="Login">keyboard_arrow_left</mat-icon>
BACK TO EDIT
</button>
<button
[disabled]="control.invalid || !!(progress$ | async)"
color="primary"
mat-button
(click)="review ? commit() : reviewChanges()"
>
{{ review ? 'COMMIT' : 'REVIEW' }}
<mat-icon aria-label="Login">keyboard_arrow_right</mat-icon>
</button>
</cc-actions>
</div>
</div>

View File

@ -0,0 +1,93 @@
import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { DomainObject } from '@vality/domain-proto/lib/domain';
import { from, BehaviorSubject } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators';
import { progressTo, getUnionKey, enumHasValue } from '../../../utils';
import { EditorKind } from '../../shared/components/thrift-editor';
import { ViewerKind } from '../../shared/components/thrift-viewer';
import { DomainMetadataFormExtensionsService } from '../../shared/services';
import { ErrorService } from '../../shared/services/error';
import { NotificationService } from '../../shared/services/notification';
import { DomainStoreService } from '../../thrift-services/damsel/domain-store.service';
import { DomainNavigateService } from '../services/domain-navigate.service';
import { MetadataService } from '../services/metadata.service';
const EDITOR_KIND = 'domain-obj-creation-editor-kind';
const REVIEW_KIND = 'domain-obj-creation-review-kind';
@UntilDestroy()
@Component({
templateUrl: './domain-obj-creation.component.html',
styleUrls: ['../editor-container.scss'],
})
export class DomainObjCreationComponent {
control = new FormControl<DomainObject>(null, Validators.required);
review = false;
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
extensions$ = this.domainMetadataFormExtensionsService.extensions$;
progress$ = new BehaviorSubject(0);
get kind() {
const kind = localStorage.getItem(EDITOR_KIND);
if (!enumHasValue(EditorKind, kind)) {
this.kind = EditorKind.Form;
return EditorKind.Form;
}
return kind;
}
set kind(kind: EditorKind) {
localStorage.setItem(EDITOR_KIND, kind);
}
get reviewKind() {
const kind = localStorage.getItem(REVIEW_KIND);
if (!enumHasValue(ViewerKind, kind)) {
this.reviewKind = ViewerKind.Editor;
return ViewerKind.Editor;
}
return kind;
}
set reviewKind(kind: ViewerKind) {
localStorage.setItem(REVIEW_KIND, kind);
}
constructor(
private domainMetadataFormExtensionsService: DomainMetadataFormExtensionsService,
private domainStoreService: DomainStoreService,
private notificationService: NotificationService,
private errorService: ErrorService,
private router: Router,
private domainNavigateService: DomainNavigateService,
private metadataService: MetadataService
) {}
reviewChanges() {
this.review = true;
}
commit() {
this.domainStoreService
.commit({ ops: [{ insert: { object: this.control.value } }] })
.pipe(
withLatestFrom(
this.metadataService.getDomainFieldByFieldName(getUnionKey(this.control.value))
),
progressTo(this.progress$),
untilDestroyed(this)
)
.subscribe({
next: ([, field]) => {
this.notificationService.success('Successfully created');
void this.domainNavigateService.toType(String(field.type));
},
error: (err) => {
this.errorService.error(err);
},
});
}
}

View File

@ -0,0 +1,38 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterModule } from '@angular/router';
import { ActionsModule } from '@vality/ng-core';
import { ThriftEditorModule } from '@cc/app/shared/components/thrift-editor';
import { MonacoEditorModule } from '../../monaco-editor';
import { ThriftViewerModule } from '../../shared/components/thrift-viewer';
import { DomainObjCreationComponent } from './domain-obj-creation.component';
@NgModule({
declarations: [DomainObjCreationComponent],
imports: [
CommonModule,
FlexLayoutModule,
RouterModule,
MatProgressSpinnerModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MonacoEditorModule,
MatDialogModule,
ReactiveFormsModule,
ThriftEditorModule,
ActionsModule,
ThriftViewerModule,
],
exports: [DomainObjCreationComponent],
})
export class DomainObjCreationModule {}

View File

@ -0,0 +1,2 @@
export * from './domain-obj-creation.module';
export * from './domain-obj-creation.component';

View File

@ -1,30 +1,42 @@
<div class="editor-container">
<div *ngIf="isLoading" fxLayout fxLayoutAlign="center stretch"><mat-spinner></mat-spinner></div>
<mat-card *ngIf="initialized">
<mat-card-header>
<mat-card-title>Edit {{ model.objectType }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<cc-monaco-editor
[codeLensProviders]="codeLensProviders"
[completionProviders]="completionProviders"
[file]="modifiedFile"
class="editor"
(fileChange)="fileChange($event)"
></cc-monaco-editor>
</mat-card-content>
<mat-card-actions>
<div fxLayout="row" fxLayoutAlign="space-between center">
<button mat-button routerLink="/domain">
<div *ngIf="progress$ | async; else content" fxLayout fxLayoutAlign="center stretch">
<mat-spinner></mat-spinner>
</div>
<ng-template #content>
<div fxLayout="column" fxLayoutGap="24px">
<div class="cc-display-1">Edit {{ type$ | async }}</div>
<mat-card>
<mat-card-content class="content">
<cc-thrift-editor
[codeLensProviders]="codeLensProviders"
[completionProviders]="completionProviders"
[defaultValue]="object$ | async"
[extensions]="extensions$ | async"
[formControl]="control"
[kind]="kind"
[metadata]="metadata$ | async"
[type]="type$ | async"
class="editor"
namespace="domain"
(changeKind)="kind = $event"
></cc-thrift-editor>
</mat-card-content>
</mat-card>
<cc-actions>
<button mat-button (click)="backToDomain()">
<mat-icon aria-label="Login">keyboard_arrow_left</mat-icon>
BACK TO DOMAIN
</button>
<button color="warn" mat-button (click)="resetChanges()">RESET CHANGES</button>
<button [disabled]="!valid" mat-button (click)="reviewChanges()">
<button
[disabled]="control.invalid"
color="primary"
mat-button
(click)="reviewChanges()"
>
REVIEW CHANGES
<mat-icon aria-label="Login">keyboard_arrow_right</mat-icon>
</button>
</div>
</mat-card-actions>
</mat-card>
</cc-actions>
</div>
</ng-template>
</div>

View File

@ -1,98 +1,79 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { Router, ActivatedRoute } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { from } from 'rxjs';
import { first } from 'rxjs/operators';
import { CodeLensProvider, CompletionProvider, MonacoFile } from '../../monaco-editor';
import { DomainModificationModel } from '../domain-modification-model';
import { DomainReviewService } from '../domain-review.service';
import { toMonacoFile } from '../utils';
import { enumHasValue } from '../../../utils';
import { CodeLensProvider, CompletionProvider } from '../../monaco-editor';
import { EditorKind } from '../../shared/components/thrift-editor';
import { DomainMetadataFormExtensionsService } from '../../shared/services';
import { DomainNavigateService } from '../services/domain-navigate.service';
import { DomainObjModificationService } from '../services/domain-obj-modification.service';
import { ModifiedDomainObjectService } from '../services/modified-domain-object.service';
import { DomainObjCodeLensProvider } from './domain-obj-code-lens-provider';
import { DomainObjCompletionProvider } from './domain-obj-completion-provider';
import { DomainObjModificationService } from './domain-obj-modification.service';
import { ResetConfirmDialogComponent } from './reset-confirm-dialog/reset-confirm-dialog.component';
const EDITOR_KIND = 'domain-obj-modification-kind';
@UntilDestroy()
@Component({
templateUrl: './domain-obj-modification.component.html',
styleUrls: ['../editor-container.scss'],
providers: [DomainObjModificationService],
})
export class DomainObjModificationComponent implements OnInit, OnDestroy {
initialized = false;
isLoading: boolean;
valid = false;
codeLensProviders: CodeLensProvider[];
completionProviders: CompletionProvider[];
modifiedFile: MonacoFile;
model: DomainModificationModel;
export class DomainObjModificationComponent implements OnInit {
control = new FormControl();
private initSub: Subscription;
progress$ = this.domainObjModService.progress$;
codeLensProviders: CodeLensProvider[] = [new DomainObjCodeLensProvider()];
completionProviders: CompletionProvider[] = [new DomainObjCompletionProvider()];
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
object$ = this.domainObjModService.object$;
type$ = this.domainObjModService.type$;
extensions$ = this.domainMetadataFormExtensionsService.extensions$;
get kind() {
const kind = localStorage.getItem(EDITOR_KIND);
if (!enumHasValue(EditorKind, kind)) {
this.kind = EditorKind.Editor;
return EditorKind.Editor;
}
return kind;
}
set kind(kind: EditorKind) {
localStorage.setItem(EDITOR_KIND, kind);
}
constructor(
private router: Router,
private route: ActivatedRoute,
private snackBar: MatSnackBar,
private domainObjModService: DomainObjModificationService,
private domainReviewService: DomainReviewService,
private dialog: MatDialog
private modifiedDomainObjectService: ModifiedDomainObjectService,
private domainMetadataFormExtensionsService: DomainMetadataFormExtensionsService,
private domainNavigateService: DomainNavigateService
) {}
ngOnInit() {
this.initSub = this.initialize();
this.codeLensProviders = [new DomainObjCodeLensProvider()];
this.completionProviders = [new DomainObjCompletionProvider()];
}
ngOnDestroy() {
if (this.initSub) {
this.initSub.unsubscribe();
}
}
fileChange({ content }: MonacoFile) {
const modified = this.domainObjModService.modify(this.model.original, content);
this.valid = !!modified;
this.model.modified = modified;
this.domainObjModService.object$.pipe(first(), untilDestroyed(this)).subscribe((object) => {
if (
this.modifiedDomainObjectService.domainObject &&
this.route.snapshot.params.ref === this.modifiedDomainObjectService.ref
)
this.control.setValue(this.modifiedDomainObjectService.domainObject);
else this.control.setValue(object);
});
}
reviewChanges() {
this.domainReviewService.addReviewModel(this.model);
void this.router.navigate(['domain', JSON.stringify(this.model.ref), 'review']);
this.modifiedDomainObjectService.update(this.control.value, this.route.snapshot.params.ref);
void this.router.navigate(['domain', 'edit', this.route.snapshot.params.ref, 'review']);
}
resetChanges() {
this.dialog
.open(ResetConfirmDialogComponent, {
width: '300px',
})
.afterClosed()
.subscribe((result) => {
if (!result) {
return;
}
const modified = this.domainObjModService.reset(this.model.original);
this.model.modified = modified;
this.modifiedFile = toMonacoFile(modified.monacoContent);
});
}
private initialize(): Subscription {
this.isLoading = true;
return this.domainObjModService.init().subscribe(
(model) => {
this.isLoading = false;
this.model = model;
this.modifiedFile = toMonacoFile(model.modified.monacoContent);
this.initialized = true;
if (this.initSub) {
this.initSub.unsubscribe();
}
},
(err) => {
console.error(err);
this.isLoading = false;
this.snackBar.open(`An error occurred while initializing: ${String(err)}`, 'OK');
}
);
backToDomain() {
this.type$.pipe(first()).subscribe((type) => this.domainNavigateService.toType(type));
}
}

View File

@ -1,19 +1,22 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterModule } from '@angular/router';
import { ActionsModule } from '@vality/ng-core';
import { ThriftEditorModule } from '@cc/app/shared/components/thrift-editor';
import { MonacoEditorModule } from '../../monaco-editor';
import { DomainObjModificationComponent } from './domain-obj-modification.component';
import { ResetConfirmDialogComponent } from './reset-confirm-dialog/reset-confirm-dialog.component';
@NgModule({
declarations: [DomainObjModificationComponent, ResetConfirmDialogComponent],
declarations: [DomainObjModificationComponent],
imports: [
CommonModule,
FlexLayoutModule,
@ -24,6 +27,9 @@ import { ResetConfirmDialogComponent } from './reset-confirm-dialog/reset-confir
MatIconModule,
MonacoEditorModule,
MatDialogModule,
ReactiveFormsModule,
ThriftEditorModule,
ActionsModule,
],
exports: [DomainObjModificationComponent],
})

View File

@ -1,130 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DomainObject, Reference } from '@vality/domain-proto/lib/domain';
import { combineLatest, Observable, of, Subject } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { MetaApplicator } from '../../damsel-meta/meta-applicator.service';
import { MetaBuilder } from '../../damsel-meta/meta-builder.service';
import { ThriftType } from '../../damsel-meta/thrift-builder';
import { ThriftBuilderService } from '../../damsel-meta/thrift-builder.service';
import { getThriftInstance } from '../../thrift-services';
import { DomainModificationModel, ModificationItem } from '../domain-modification-model';
import { DomainReviewService } from '../domain-review.service';
import { DomainService } from '../domain.service';
import { MetadataService } from '../metadata.service';
import { parseRef, toMonacoContent } from '../utils';
@Injectable()
export class DomainObjModificationService {
private errors$: Subject<string> = new Subject();
get errors(): Observable<string> {
return this.errors$;
}
constructor(
private route: ActivatedRoute,
private domainService: DomainService,
private metadataService: MetadataService,
private metaBuilder: MetaBuilder,
private metaApplicator: MetaApplicator,
private domainReviewService: DomainReviewService,
private thriftBuilderService: ThriftBuilderService
) {
this.metaBuilder.errors.subscribe((e) => {
this.errors$.next(e);
console.error('Build meta error:', e);
});
this.metaApplicator.errors.subscribe((e) => console.error('Apply meta error:', e));
}
init(namespace = 'domain'): Observable<DomainModificationModel> {
return combineLatest([this.route.params, this.domainReviewService.reviewModel]).pipe(
switchMap(([routeParams, model]) => {
if (model && JSON.stringify(model.ref) === routeParams.ref) {
return of(model);
}
const ref = parseRef(routeParams.ref);
return combineLatest([
this.metadataService.getDomainObjectType(ref),
this.domainService.getDomainObject(ref),
]).pipe(
switchMap(([objectType, domainObj]) =>
this.build(ref, objectType, domainObj, namespace)
)
);
})
);
}
modify(original: ModificationItem, modifiedContent: string): ModificationItem | null {
const modifiedMeta = this.metaApplicator.apply(original.meta, modifiedContent);
if (!modifiedMeta) {
return;
}
const thrift = this.thriftBuilderService.build(modifiedMeta);
if (!thrift) {
return;
}
return {
meta: modifiedMeta,
domainObj: this.formNewDomainObj(original.domainObj, thrift),
monacoContent: modifiedContent,
};
}
reset({ monacoContent }: ModificationItem): Partial<ModificationItem> {
return {
monacoContent: monacoContent.slice(),
};
}
private build(
ref: Reference,
objectType: string,
domainObj: DomainObject,
namespace: string
): Observable<DomainModificationModel> {
if (!objectType) {
throw new Error('Domain object type not found');
}
if (!domainObj) {
throw new Error('Domain object not found');
}
return this.metaBuilder.build(objectType, namespace).pipe(
map((initialMeta) => {
if (!initialMeta) {
throw new Error('Build initial meta failed');
}
const monacoContent = toMonacoContent(domainObj);
const applied = this.metaApplicator.apply(initialMeta, monacoContent);
if (!applied) {
throw new Error('Apply original value failed');
}
return {
ref,
objectType,
original: {
monacoContent,
domainObj,
meta: applied,
},
modified: {
monacoContent: monacoContent.slice(),
},
};
})
);
}
private formNewDomainObj(source: DomainObject, thrift: ThriftType): DomainObject {
const result = getThriftInstance('domain', 'DomainObject');
const filtered = Object.keys(source).filter((k) => !!source[k]);
if (filtered.length !== 1) {
throw new Error('Should be only one field in DomainObject');
}
result[filtered[0]] = thrift;
return result;
}
}

View File

@ -1,5 +0,0 @@
<div mat-dialog-title>Confirm reset changes</div>
<div mat-dialog-actions>
<button [mat-dialog-close] mat-button>CANCEL</button>
<button [mat-dialog-close]="'true'" mat-button>CONFIRM</button>
</div>

View File

@ -1,6 +0,0 @@
import { Component } from '@angular/core';
@Component({
templateUrl: 'reset-confirm-dialog.component.html',
})
export class ResetConfirmDialogComponent {}

View File

@ -1,33 +1,34 @@
<div class="editor-container">
<mat-card *ngIf="initialized">
<mat-card-header fxLayout="row" fxLayoutAlign="space-between stretch">
<mat-card-title>Review changes of {{ objectType }}</mat-card-title>
<div>
<mat-checkbox
[checked]="options.renderSideBySide"
(change)="renderSideBySide($event)"
>Side by side</mat-checkbox
>
</div>
</mat-card-header>
<mat-card-content>
<cc-monaco-diff-editor
[modified]="modified"
[options]="options"
[original]="original"
class="editor"
></cc-monaco-diff-editor>
</mat-card-content>
<mat-card-actions>
<div fxLayout="row" fxLayoutAlign="space-between center">
<button mat-button (click)="back()">
<div *ngIf="progress$ | async; else content" fxLayout fxLayoutAlign="center stretch">
<mat-spinner></mat-spinner>
</div>
<ng-template #content>
<div fxLayout="column" fxLayoutGap="24px">
<div class="cc-display-1">Review changes of {{ type$ | async }}</div>
<mat-card>
<mat-card-content class="content">
<cc-thrift-viewer
[compared]="modifiedObject"
[value]="object$ | async"
class="editor"
kind="diff"
></cc-thrift-viewer>
</mat-card-content>
</mat-card>
<cc-actions>
<button [disabled]="!!(progress$ | async)" mat-button (click)="back()">
<mat-icon>keyboard_arrow_left</mat-icon>
BACK TO EDIT
</button>
<button [disabled]="isLoading" color="primary" mat-button (click)="commit()">
<button
[disabled]="!!(progress$ | async)"
color="primary"
mat-button
(click)="commit()"
>
COMMIT
</button>
</div>
</mat-card-actions>
</mat-card>
</cc-actions>
</div>
</ng-template>
</div>

View File

@ -1,90 +1,79 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { switchMap } from 'rxjs';
import { first, withLatestFrom } from 'rxjs/operators';
import { IDiffEditorOptions, MonacoFile } from '../../monaco-editor';
import { DomainModificationModel } from '../domain-modification-model';
import { toMonacoFile } from '../utils';
import { DomainObjReviewService } from './domain-obj-review.service';
import { getUnionKey } from '../../../utils';
import { ErrorService } from '../../shared/services/error';
import { NotificationService } from '../../shared/services/notification';
import { DomainStoreService } from '../../thrift-services/damsel/domain-store.service';
import { DomainNavigateService } from '../services/domain-navigate.service';
import { DomainObjModificationService } from '../services/domain-obj-modification.service';
import { ModifiedDomainObjectService } from '../services/modified-domain-object.service';
@UntilDestroy()
@Component({
templateUrl: './domain-obj-review.component.html',
styleUrls: ['../editor-container.scss'],
providers: [DomainObjReviewService],
providers: [DomainObjModificationService],
})
export class DomainObjReviewComponent implements OnInit, OnDestroy {
initialized = false;
original: MonacoFile;
modified: MonacoFile;
objectType: string;
options: IDiffEditorOptions = {
renderSideBySide: true,
readOnly: true,
};
isLoading = false;
private reviewModelSub: Subscription;
private ref: string;
private model: DomainModificationModel;
export class DomainObjReviewComponent {
progress$ = this.domainObjModService.progress$;
object$ = this.domainObjModService.object$;
type$ = this.domainObjModService.type$;
modifiedObject = this.modifiedDomainObjectService.domainObject;
constructor(
private router: Router,
private snackBar: MatSnackBar,
private domainObjReviewService: DomainObjReviewService
) {}
ngOnInit() {
this.initialize();
}
ngOnDestroy() {
if (this.reviewModelSub) {
this.reviewModelSub.unsubscribe();
private route: ActivatedRoute,
private domainObjModService: DomainObjModificationService,
private modifiedDomainObjectService: ModifiedDomainObjectService,
private domainStoreService: DomainStoreService,
private notificationService: NotificationService,
private errorService: ErrorService,
private domainNavigateService: DomainNavigateService
) {
if (!modifiedDomainObjectService.domainObject) {
this.back();
}
}
renderSideBySide({ checked }: MatCheckboxChange) {
this.options = { ...this.options, renderSideBySide: checked };
commit() {
this.domainObjModService.fullObject$
.pipe(
first(),
// eslint-disable-next-line @typescript-eslint/naming-convention
switchMap((old_object) =>
this.domainStoreService.commit({
ops: [
{
update: {
old_object,
new_object: {
[getUnionKey(old_object)]: this.modifiedObject,
},
},
},
],
})
),
withLatestFrom(this.type$),
// progressTo(this.progress$),
untilDestroyed(this)
)
.subscribe({
next: ([, type]) => {
this.notificationService.success('Successfully changed');
void this.domainNavigateService.toType(type);
},
error: (err) => {
this.errorService.error(err);
},
});
}
back() {
void this.router.navigate(['domain', this.ref]);
}
commit() {
this.isLoading = true;
this.domainObjReviewService.commit(this.model).subscribe(
() => {
this.isLoading = false;
this.snackBar.open('Commit successful', 'OK', {
duration: 2000,
});
void this.router.navigate(['domain', this.ref]);
},
(ex) => {
this.isLoading = false;
console.error(ex);
this.snackBar.open(`An error occured while commit: ${String(ex)}`, 'OK');
}
);
}
private initialize() {
this.reviewModelSub = this.domainObjReviewService
.initialize()
.subscribe(([{ ref }, model]) => {
this.ref = ref;
if (!model) {
this.back();
return;
}
this.original = toMonacoFile(model.original.monacoContent);
this.modified = toMonacoFile(model.modified.monacoContent);
this.objectType = model.objectType;
this.model = model;
this.initialized = true;
});
void this.router.navigate(['domain', 'edit', this.route.snapshot.params.ref]);
}
}

View File

@ -1,13 +1,18 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterModule } from '@angular/router';
import { ActionsModule } from '@vality/ng-core';
import { MonacoEditorModule } from '../../monaco-editor';
import { ThriftEditorModule } from '../../shared/components/thrift-editor';
import { ThriftViewerModule } from '../../shared/components/thrift-viewer';
import { DomainObjReviewComponent } from './domain-obj-review.component';
@NgModule({
@ -21,6 +26,11 @@ import { DomainObjReviewComponent } from './domain-obj-review.component';
MatCheckboxModule,
MonacoEditorModule,
MatIconModule,
ThriftEditorModule,
MatProgressSpinnerModule,
ActionsModule,
ReactiveFormsModule,
ThriftViewerModule,
],
exports: [DomainObjReviewComponent],
})

View File

@ -1,38 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Version } from '@vality/domain-proto/lib/domain_config';
import { combineLatest, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { DomainModificationModel } from '../domain-modification-model';
import { DomainReviewService } from '../domain-review.service';
import { DomainService } from '../domain.service';
@Injectable()
export class DomainObjReviewService {
constructor(
private route: ActivatedRoute,
private domainService: DomainService,
private domainReviewService: DomainReviewService
) {}
commit({ original, modified }: DomainModificationModel): Observable<Version> {
const commit = {
ops: [
{
update: {
old_object: original.domainObj,
new_object: modified.domainObj,
},
},
],
};
return this.domainService
.commit(commit)
.pipe(tap(() => this.domainReviewService.resetModel()));
}
initialize() {
return combineLatest([this.route.params, this.domainReviewService.reviewModel]);
}
}

View File

@ -1,24 +0,0 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { DomainModificationModel } from './domain-modification-model';
@Injectable()
export class DomainReviewService {
private reviewModel$: Subject<DomainModificationModel> = new BehaviorSubject(null);
get reviewModel(): Observable<DomainModificationModel> {
return this.reviewModel$;
}
addReviewModel(model: DomainModificationModel) {
if (!model) {
return;
}
this.reviewModel$.next(model);
}
resetModel() {
this.reviewModel$.next(null);
}
}

View File

@ -4,6 +4,7 @@ import { RouterModule } from '@angular/router';
import { AppAuthGuardService, DomainConfigRole } from '@cc/app/shared/services';
import { DomainInfoComponent } from './domain-info';
import { DomainObjCreationComponent } from './domain-obj-creation';
import { DomainObjModificationComponent } from './domain-obj-modification';
import { DomainObjReviewComponent } from './domain-obj-review';
@ -19,7 +20,15 @@ import { DomainObjReviewComponent } from './domain-obj-review';
},
},
{
path: 'domain/:ref',
path: 'domain/create',
component: DomainObjCreationComponent,
canActivate: [AppAuthGuardService],
data: {
roles: [DomainConfigRole.Checkout],
},
},
{
path: 'domain/edit/:ref',
component: DomainObjModificationComponent,
canActivate: [AppAuthGuardService],
data: {
@ -27,7 +36,7 @@ import { DomainObjReviewComponent } from './domain-obj-review';
},
},
{
path: 'domain/:ref/review',
path: 'domain/edit/:ref/review',
component: DomainObjReviewComponent,
canActivate: [AppAuthGuardService],
data: {

View File

@ -1,13 +1,11 @@
import { NgModule } from '@angular/core';
import { DamselMetaModule } from '../damsel-meta/damsel-meta.module';
import { DomainInfoModule } from './domain-info/domain-info.module';
import { DomainObjModificationModule } from './domain-obj-modification';
import { DomainObjReviewModule } from './domain-obj-review';
import { DomainReviewService } from './domain-review.service';
import { DomainRoutingModule } from './domain-routing.module';
import { DomainService } from './domain.service';
import { MetadataService } from './metadata.service';
import { MetadataService } from './services/metadata.service';
import { ModifiedDomainObjectService } from './services/modified-domain-object.service';
@NgModule({
imports: [
@ -15,8 +13,7 @@ import { MetadataService } from './metadata.service';
DomainInfoModule,
DomainObjModificationModule,
DomainObjReviewModule,
DamselMetaModule,
],
providers: [DomainService, MetadataService, DomainReviewService],
providers: [MetadataService, ModifiedDomainObjectService],
})
export class DomainModule {}

View File

@ -1,75 +0,0 @@
import { Injectable } from '@angular/core';
import { DomainObject, Reference } from '@vality/domain-proto/lib/domain';
import { Commit, Snapshot } from '@vality/domain-proto/lib/domain_config';
import { Int64 } from '@vality/thrift-ts';
import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { toJson } from '@cc/utils/thrift-json-converter';
import { toGenCommit, toGenReference } from '../thrift-services/converters';
import { DomainService as ThriftDomainService } from '../thrift-services/damsel/domain.service';
/**
* @deprecated duplicates thrift-services/damsel/domain-cache.service
*/
@Injectable()
export class DomainService {
private shapshot$: Observable<Snapshot>;
constructor(private thriftDomainService: ThriftDomainService) {
this.updateSnapshot();
}
/**
* @deprecated use DomainCacheService -> snapshot$
*/
get shapshot() {
return this.shapshot$;
}
/**
* @deprecated use DomainCacheService -> version$
*/
get version$(): Observable<number> {
return this.shapshot$.pipe(
map(({ version }) => (version ? (version as unknown as Int64).toNumber() : undefined))
);
}
/**
* @deprecated use DomainCacheService -> getObjects or specific service from thrift-services/damsel
*/
getDomainObject(ref: Reference): Observable<DomainObject | null> {
return this.shapshot$.pipe(
map(({ domain }) => {
const searchRef = JSON.stringify(ref);
for (const [k, v] of domain) {
const domainRef = JSON.stringify(toJson(k));
if (domainRef === searchRef) {
return v;
}
}
return null;
})
);
}
/**
* @deprecated use DomainCacheService -> forceReload()
*/
updateSnapshot() {
return (this.shapshot$ = this.thriftDomainService.checkout(toGenReference()));
}
/**
* @deprecated use DomainCacheService -> commit()
*/
commit(commit: Commit) {
return this.shapshot$.pipe(
switchMap(({ version }) =>
this.thriftDomainService.commit(version, toGenCommit(commit))
)
);
}
}

View File

@ -1,8 +1,23 @@
$offset: 24px;
.editor {
display: block;
height: calc(100vh - 200px);
height: 100%;
}
.editor-container {
margin: 10px;
margin: $offset 16px 0;
}
.content {
$header-height: 64px;
$title-height: 40px;
$actions-height: 36px;
$offsets: $offset * 4;
$internal-offsets: 16px * 2;
height: calc(
100vh - ($header-height + $title-height + $actions-height + $offsets + $internal-offsets)
);
overflow-y: scroll;
}

View File

@ -1,2 +1 @@
export * from './domain.module';
export * from './domain.service';

View File

@ -0,0 +1,13 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class DomainNavigateService {
constructor(private router: Router) {}
toType(type: string) {
return this.router.navigate(['domain'], { queryParams: { types: JSON.stringify([type]) } });
}
}

View File

@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DomainObject, Reference } from '@vality/domain-proto/lib/domain';
import { Observable, switchMap, BehaviorSubject, defer } from 'rxjs';
import { map, shareReplay, first } from 'rxjs/operators';
import { ErrorService } from '@cc/app/shared/services/error';
import { DomainStoreService } from '@cc/app/thrift-services/damsel/domain-store.service';
import { toJson, getUnionValue, progressTo } from '@cc/utils';
import { MetadataService } from './metadata.service';
@Injectable()
export class DomainObjModificationService {
progress$ = new BehaviorSubject(0);
fullObject$ = defer(() => this.ref$).pipe(
switchMap((ref) => this.getDomainObject(ref).pipe(progressTo(this.progress$))),
shareReplay({ refCount: true, bufferSize: 1 })
);
object$ = this.fullObject$.pipe(
map((obj) => getUnionValue(obj)),
shareReplay({ refCount: true, bufferSize: 1 })
);
type$ = defer(() => this.ref$).pipe(
switchMap((ref) => this.metadataService.getDomainObjectType(ref)),
shareReplay({ refCount: true, bufferSize: 1 })
);
private ref$ = this.route.params.pipe(
map(({ ref }) => {
try {
return JSON.parse(ref as string) as Reference;
} catch (err) {
this.errorService.error(err, 'Malformed domain object ref');
return null;
}
})
);
constructor(
private route: ActivatedRoute,
private domainStoreService: DomainStoreService,
private metadataService: MetadataService,
private errorService: ErrorService
) {}
private getDomainObject(ref: Reference): Observable<DomainObject> {
return this.domainStoreService.domain$.pipe(
first(),
map((domain) => {
const searchRef = JSON.stringify(ref);
return domain.get(
Array.from(domain.keys()).find((k) => JSON.stringify(toJson(k)) === searchRef)
);
})
);
}
}

Some files were not shown because too many files have changed in this diff Show More