This commit is contained in:
Matt Kincaid 2020-01-23 13:28:30 -08:00
Родитель 8dbecbd4f8
Коммит f31a35597d
65 изменённых файлов: 41066 добавлений и 0 удалений

13
.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

7
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################
/node_modules/
/.vs/
notcheckedin/

Просмотреть файл

@ -1,3 +1,65 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
-->
## Standards Mapping
Standards Mapping is an open source tool used to map and visualize standards and regulations.
### Running the app
You can access the app live at the following URL: https://mkslalom.github.io/standards-mapping/.
### Building the code
#### Prerequisites
Please ensure that you have at least the **minimum** recommended versions
- Node >= 9.0.0
#### 1. Clone the repository
- Clone the repository using one of the following commands
```bash
git clone https://github.com/mkslalom/standards-mapping.git
```
or
```bash
git clone git@github.com:mkslalom/standards-mapping.git
```
- Select the created directory
```bash
cd standards-mapping
```
#### 2. Install packages
- Install the packages
```bash
npm install
```
(Temporary note from Matt. You may need to install Angular CLI separately: npm install -g @angular/cli)
#### 3. Build and run
- Run the dev server
```bash
ng serve
```
#### 4. Open app in web browser
- Navigate your browser to:
http://localhost:4200/dashboard
### More Information
Please contact: Tom Wagner <tomw@slalom.com>, Matt Kincaid <matt.kincaid@slalom.com>, Douglas Branca <dougb@slalom.com>
# data-protection-mapping-project
Open Source Data Protection/Privacy Regulatory Mapping Project
Copyright <2020> <Data Protection Mapping Project>

136
angular.json Normal file
Просмотреть файл

@ -0,0 +1,136 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"angular.io-example": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css",
"node_modules/angular-tree-component/dist/angular-tree-component.css"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "angular.io-example:build"
},
"configurations": {
"production": {
"browserTarget": "angular.io-example:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "angular.io-example:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"src/styles.css"
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"angular.io-example-e2e": {
"root": "e2e/",
"projectType": "application",
"prefix": "",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "angular.io-example:serve"
},
"configurations": {
"production": {
"devServerTarget": "angular.io-example:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "angular.io-example"
}

28
e2e/protractor.conf.js Normal file
Просмотреть файл

@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome',
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.e2e.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

307
e2e/src/app.e2e-spec.ts Normal file
Просмотреть файл

@ -0,0 +1,307 @@
'use strict'; // necessary for es6 output in node
import { browser, element, by, ElementFinder, ElementArrayFinder } from 'protractor';
import { promise } from 'selenium-webdriver';
const expectedH1 = 'Tour of Heroes';
const expectedTitle = `${expectedH1}`;
const targetHero = { id: 15, name: 'Magneta' };
const targetHeroDashboardIndex = 3;
const nameSuffix = 'X';
const newHeroName = targetHero.name + nameSuffix;
class Hero {
id: number;
name: string;
// Factory methods
// Hero from string formatted as '<id> <name>'.
static fromString(s: string): Hero {
return {
id: +s.substr(0, s.indexOf(' ')),
name: s.substr(s.indexOf(' ') + 1),
};
}
// Hero from hero list <li> element.
static async fromLi(li: ElementFinder): Promise<Hero> {
let stringsFromA = await li.all(by.css('a')).getText();
let strings = stringsFromA[0].split(' ');
return { id: +strings[0], name: strings[1] };
}
// Hero id and name from the given detail element.
static async fromDetail(detail: ElementFinder): Promise<Hero> {
// Get hero id from the first <div>
let _id = await detail.all(by.css('div')).first().getText();
// Get name from the h2
let _name = await detail.element(by.css('h2')).getText();
return {
id: +_id.substr(_id.indexOf(' ') + 1),
name: _name.substr(0, _name.lastIndexOf(' '))
};
}
}
describe('Tutorial part 6', () => {
beforeAll(() => browser.get(''));
function getPageElts() {
let navElts = element.all(by.css('app-root nav a'));
return {
navElts: navElts,
appDashboardHref: navElts.get(0),
appDashboard: element(by.css('app-root app-dashboard')),
topHeroes: element.all(by.css('app-root app-dashboard > div h4')),
appHeroesHref: navElts.get(1),
appHeroes: element(by.css('app-root app-heroes')),
allHeroes: element.all(by.css('app-root app-heroes li')),
selectedHeroSubview: element(by.css('app-root app-heroes > div:last-child')),
heroDetail: element(by.css('app-root app-hero-detail > div')),
searchBox: element(by.css('#search-box')),
searchResults: element.all(by.css('.search-result li'))
};
}
describe('Initial page', () => {
it(`has title '${expectedTitle}'`, () => {
expect(browser.getTitle()).toEqual(expectedTitle);
});
it(`has h1 '${expectedH1}'`, () => {
expectHeading(1, expectedH1);
});
const expectedViewNames = ['Dashboard', 'Heroes'];
it(`has views ${expectedViewNames}`, () => {
let viewNames = getPageElts().navElts.map((el: ElementFinder) => el.getText());
expect(viewNames).toEqual(expectedViewNames);
});
it('has dashboard as the active view', () => {
let page = getPageElts();
expect(page.appDashboard.isPresent()).toBeTruthy();
});
});
describe('Dashboard tests', () => {
beforeAll(() => browser.get(''));
it('has top heroes', () => {
let page = getPageElts();
expect(page.topHeroes.count()).toEqual(4);
});
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
it(`cancels and shows ${targetHero.name} in Dashboard`, () => {
element(by.buttonText('go back')).click();
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
expect(targetHeroElt.getText()).toEqual(targetHero.name);
});
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
it(`saves and shows ${newHeroName} in Dashboard`, () => {
element(by.buttonText('save')).click();
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
expect(targetHeroElt.getText()).toEqual(newHeroName);
});
});
describe('Heroes tests', () => {
beforeAll(() => browser.get(''));
it('can switch to Heroes view', () => {
getPageElts().appHeroesHref.click();
let page = getPageElts();
expect(page.appHeroes.isPresent()).toBeTruthy();
expect(page.allHeroes.count()).toEqual(10, 'number of heroes');
});
it('can route to hero details', async () => {
getHeroLiEltById(targetHero.id).click();
let page = getPageElts();
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
let hero = await Hero.fromDetail(page.heroDetail);
expect(hero.id).toEqual(targetHero.id);
expect(hero.name).toEqual(targetHero.name.toUpperCase());
});
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
it(`shows ${newHeroName} in Heroes list`, () => {
element(by.buttonText('save')).click();
browser.waitForAngular();
let expectedText = `${targetHero.id} ${newHeroName}`;
expect(getHeroAEltById(targetHero.id).getText()).toEqual(expectedText);
});
it(`deletes ${newHeroName} from Heroes list`, async () => {
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
const li = getHeroLiEltById(targetHero.id);
li.element(by.buttonText('x')).click();
const page = getPageElts();
expect(page.appHeroes.isPresent()).toBeTruthy();
expect(page.allHeroes.count()).toEqual(9, 'number of heroes');
const heroesAfter = await toHeroArray(page.allHeroes);
// console.log(await Hero.fromLi(page.allHeroes[0]));
const expectedHeroes = heroesBefore.filter(h => h.name !== newHeroName);
expect(heroesAfter).toEqual(expectedHeroes);
// expect(page.selectedHeroSubview.isPresent()).toBeFalsy();
});
it(`adds back ${targetHero.name}`, async () => {
const newHeroName = 'Alice';
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
const numHeroes = heroesBefore.length;
element(by.css('input')).sendKeys(newHeroName);
element(by.buttonText('add')).click();
let page = getPageElts();
let heroesAfter = await toHeroArray(page.allHeroes);
expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes');
expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there');
const maxId = heroesBefore[heroesBefore.length - 1].id;
expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: newHeroName});
});
it('displays correctly styled buttons', async () => {
element.all(by.buttonText('x')).then(buttons => {
for (const button of buttons) {
// Inherited styles from styles.css
expect(button.getCssValue('font-family')).toBe('Arial');
expect(button.getCssValue('border')).toContain('none');
expect(button.getCssValue('padding')).toBe('5px 10px');
expect(button.getCssValue('border-radius')).toBe('4px');
// Styles defined in heroes.component.css
expect(button.getCssValue('left')).toBe('194px');
expect(button.getCssValue('top')).toBe('-32px');
}
});
const addButton = element(by.buttonText('add'));
// Inherited styles from styles.css
expect(addButton.getCssValue('font-family')).toBe('Arial');
expect(addButton.getCssValue('border')).toContain('none');
expect(addButton.getCssValue('padding')).toBe('5px 10px');
expect(addButton.getCssValue('border-radius')).toBe('4px');
});
});
describe('Progressive hero search', () => {
beforeAll(() => browser.get(''));
it(`searches for 'Ma'`, async () => {
getPageElts().searchBox.sendKeys('Ma');
browser.sleep(1000);
expect(getPageElts().searchResults.count()).toBe(4);
});
it(`continues search with 'g'`, async () => {
getPageElts().searchBox.sendKeys('g');
browser.sleep(1000);
expect(getPageElts().searchResults.count()).toBe(2);
});
it(`continues search with 'e' and gets ${targetHero.name}`, async () => {
getPageElts().searchBox.sendKeys('n');
browser.sleep(1000);
let page = getPageElts();
expect(page.searchResults.count()).toBe(1);
let hero = page.searchResults.get(0);
expect(hero.getText()).toEqual(targetHero.name);
});
it(`navigates to ${targetHero.name} details view`, async () => {
let hero = getPageElts().searchResults.get(0);
expect(hero.getText()).toEqual(targetHero.name);
hero.click();
let page = getPageElts();
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
let hero2 = await Hero.fromDetail(page.heroDetail);
expect(hero2.id).toEqual(targetHero.id);
expect(hero2.name).toEqual(targetHero.name.toUpperCase());
});
});
async function dashboardSelectTargetHero() {
let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
expect(targetHeroElt.getText()).toEqual(targetHero.name);
targetHeroElt.click();
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
let page = getPageElts();
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
let hero = await Hero.fromDetail(page.heroDetail);
expect(hero.id).toEqual(targetHero.id);
expect(hero.name).toEqual(targetHero.name.toUpperCase());
}
async function updateHeroNameInDetailView() {
// Assumes that the current view is the hero details view.
addToHeroName(nameSuffix);
let page = getPageElts();
let hero = await Hero.fromDetail(page.heroDetail);
expect(hero.id).toEqual(targetHero.id);
expect(hero.name).toEqual(newHeroName.toUpperCase());
}
});
function addToHeroName(text: string): promise.Promise<void> {
let input = element(by.css('input'));
return input.sendKeys(text);
}
function expectHeading(hLevel: number, expectedText: string): void {
let hTag = `h${hLevel}`;
let hText = element(by.css(hTag)).getText();
expect(hText).toEqual(expectedText, hTag);
};
function getHeroAEltById(id: number): ElementFinder {
let spanForId = element(by.cssContainingText('li span.badge', id.toString()));
return spanForId.element(by.xpath('..'));
}
function getHeroLiEltById(id: number): ElementFinder {
let spanForId = element(by.cssContainingText('li span.badge', id.toString()));
return spanForId.element(by.xpath('../..'));
}
async function toHeroArray(allHeroes: ElementArrayFinder): Promise<Hero[]> {
let promisedHeroes = await allHeroes.map(Hero.fromLi);
// The cast is necessary to get around issuing with the signature of Promise.all()
return <Promise<any>> Promise.all(promisedHeroes);
}

11
e2e/src/app.po.ts Normal file
Просмотреть файл

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}

13
e2e/tsconfig.e2e.json Normal file
Просмотреть файл

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

11913
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

69
package.json Normal file
Просмотреть файл

@ -0,0 +1,69 @@
{
"name": "angular-io-example",
"version": "1.0.0",
"private": true,
"description": "Example project from an angular.io guide.",
"scripts": {
"lint": "tslint ./src/**/*.ts -t verbose",
"start": "ng serve",
"test": "ng test",
"build": "ng build --aot=true --prod=true --outputPath=docs",
"e2e": "ng e2e",
"ng": "ng",
"import": "node utils/importer.js",
"convert": "node utils/converter.js"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@angular/animations": "^7.1.0",
"@angular/cdk": "~7.3.7",
"@angular/common": "^7.1.0",
"@angular/compiler": "^7.1.0",
"@angular/core": "^7.1.0",
"@angular/flex-layout": "^7.0.0-beta.24",
"@angular/forms": "^7.1.0",
"@angular/http": "^7.1.0",
"@angular/material": "^7.3.7",
"@angular/platform-browser": "^7.1.0",
"@angular/platform-browser-dynamic": "^7.1.0",
"@angular/router": "^7.1.0",
"@angular/upgrade": "^7.1.0",
"@types/d3-sankey": "^0.11.0",
"angular-in-memory-web-api": "github:brandonroberts/in-memory-web-api-bazel#50a34d8",
"angular-tree-component": "^8.3.0",
"color-convert": "^2.0.0",
"core-js": "^2.5.4",
"d3": "^5.9.2",
"d3-ng2-service": "^2.2.0",
"d3-sankey": "^0.12.1",
"d3-selection-multi": "^1.0.1",
"exceljs": "^1.11.0",
"fuse.js": "^3.4.5",
"rxjs": "^6.5.1",
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.11.0",
"@angular/cli": "^7.1.0",
"@angular/compiler-cli": "^7.1.0",
"@angular/platform-server": "^7.1.0",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "^2.0.4",
"@types/node": "~8.9.4",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"lodash": "^4.16.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.1.1"
},
"repository": {}
}

Просмотреть файл

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
import { StandardMapsComponent } from './standard-maps/standard-maps.component';
import { StandardMapDetailComponent } from './standard-map-detail/standard-map-detail.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'doctypes/:id', component: StandardMapDetailComponent },
{ path: 'standard-map', component: StandardMapsComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes, { useHash:true }) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}

41
src/app/app.component.css Normal file
Просмотреть файл

@ -0,0 +1,41 @@
.example-icon {
padding: 0 14px;
}
.example-spacer {
flex: 1 1 auto;
}
.app-body {
overflow: auto;
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 64px;
}
.app-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
mat-sidenav-container {
width: 100% !important;
height: calc(100vh - 64px) !important;
}
mat-sidenav {
background-color: red !important;
}
mat-toolbar {
height: 64px;
position: relative;
z-index: 100;
}

Просмотреть файл

@ -0,0 +1,25 @@
<div class="mat-app-background basic-container">
<mat-toolbar class="mat-elevation-z6" color="primary">
<mat-toolbar-row>
<span>{{title}}</span>
<span class="example-spacer"></span>
<a href="https://github.com/mkslalom/standards-mapping"><img src="assets/icons/github-white.svg" /></a>
</mat-toolbar-row>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav mode="side"
opened="false"
[fixedInViewport]="true"
[fixedTopGap]="64">
sidenav content
</mat-sidenav>
<router-outlet></router-outlet>
</mat-sidenav-container>
<!--< nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/standard-map">Standard Maps</a>
</!--nav>-->
<!--<app-messages></app-messages>-->
</div>

10
src/app/app.component.ts Normal file
Просмотреть файл

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Standard Maps';
}

127
src/app/app.module.ts Normal file
Просмотреть файл

@ -0,0 +1,127 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { StandardMapDetailComponent } from './standard-map-detail/standard-map-detail.component';
import { StandardMapsComponent } from './standard-maps/standard-maps.component';
import { StandardMapSearchComponent } from './standard-map-search/standard-map-search.component';
import { MessagesComponent } from './messages/messages.component';
import { D3TestComponent } from './d3-test/d3-test.component';
import { TreeModule } from 'angular-tree-component';
import {
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatDatepickerModule,
MatDialogModule,
MatDividerModule,
MatExpansionModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatStepperModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatTreeModule,
} from '@angular/material';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FlexLayoutModule } from '@angular/flex-layout';
@NgModule({
imports: [
BrowserModule,
FormsModule,
AppRoutingModule,
HttpClientModule,
//// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
//// and returns simulated server responses.
//// Remove it when a real server is ready to receive requests.
//HttpClientInMemoryWebApiModule.forRoot(
// InMemoryDataService, { dataEncapsulation: false }
//),
TreeModule.forRoot(),
// Material modules
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatStepperModule,
MatDatepickerModule,
MatDialogModule,
MatDividerModule,
MatExpansionModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatTreeModule,
BrowserAnimationsModule,
FlexLayoutModule
],
declarations: [
AppComponent,
DashboardComponent,
StandardMapsComponent,
StandardMapDetailComponent,
MessagesComponent,
StandardMapSearchComponent,
D3TestComponent
],
//providers: [D3Service],
bootstrap: [ AppComponent ]
})
export class AppModule { }

Просмотреть файл

@ -0,0 +1,176 @@

.container {
width:100%;
}
h1 {
font-size: 1.2em;
color: #999;
margin-bottom: 0;
}
.node {
}
.link {
stroke: #999;
stroke-opacity: .6;
stroke-width: 1px;
}
.graph-container {
position: relative;
}
.graph-categories {
vertical-align: top;
display: inline-block;
width: 25%;
overflow: hidden;
}
.node-content-wrapper {
border: 1px solid #d6d6d6;
width: 100%;
padding: 0px 5px;
}
.d3 {
display: inline-block;
width: 74%;
}
.table-wrapper {
position: relative;
display: inline-block;
width: 70%;
}
table, th, td {
table-layout: fixed;
border: 1px solid black;
border-collapse: collapse;
width: 100%;
}
.graph-tab-buttons {
}
.graph-tab-button {
display: inline-block;
margin: 2px;
}
.button-active {
border: 1px solid green;
}
.graph-tabs {
}
.graph-tab {
height: 400px;
}
.graph-inactive {
display: none;
}
.graph-columns {
height: 680px;
}
.graph-column {
display: inline-block;
vertical-align: top;
margin-right: 60px;
max-width: 500px;
height: 600px;
padding-bottom: 20px;
width: 400px;
}
.example-spacer {
flex: 1 1 auto;
}
.filter-title {
display: inline-block !important;
}
.title-container {
display: flex;
flex-direction: row;
}
.tree-container {
height: 482px;
}
tree-viewport {
height: calc(100% + 32px) !important;
}
.button-disabled {
opacity: 0.15;
}
.svg-background {
position: absolute;
width: 100%;
left: 0px;
top: 60px;
height: calc(100% - 80px);
pointer-events: none;
}
.svg-line-click {
cursor: pointer;
pointer-events: all;
stroke-width: 20px;
opacity: .0;
}
.svg-line-visible {
stroke: #b7b5b5;
}
.svg-line-active {
stroke: black;
stroke-width: 2;
}
.mat-tab-label {
min-width: 50px !important;
padding: 0px !important;
}
mat-card {
width: 100% !important;
height: calc(100% - 32px) !important;
}
.mat-tab-body-content {
overflow: hidden !important;
}
mat-checkbox {
margin-left: 10px !important;
}
.filter-field {
width: 100%;
}
.fake-link {
color: blue;
text-decoration: underline;
text-underline-position: under;
cursor: pointer;
}
.coverage-box {
padding: 10px;
}

Просмотреть файл

@ -0,0 +1,130 @@
<div (window:resize)="onResize($event)">
<!-- <div class="graph-type">
<input name="options" type="radio" [(ngModel)]="graphType" [value]="0"
[checked]="graphType==0" (change)="RefreshGraph()" />
<input name="options" type="radio" [(ngModel)]="graphType" [value]="1"
[checked]="graphType==1" (change)="RefreshGraph()"/>
<input name="options" type="radio" [(ngModel)]="graphType" [value]="2"
[checked]="graphType==2" (change)="RefreshGraph()"/>
</div>-->
<div fxLayout="row" fxLayoutGap="32px" fxLayoutAlign="flex-start" class="container">
<div fxFlex="0 0 300px">
<mat-card class="graph-categories">
<div class="title-container">
<mat-card-title class="filter-title">Filter</mat-card-title>
<span class="example-spacer"></span>
<button mat-button [matMenuTriggerFor]="menu" class="add-button" [class.button-disabled]="!graphService.canAdd"><mat-icon>add_box</mat-icon></button>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let d of getMenuOptions()" (click)="graphService.addTab(d.id)">{{d.id}}</button>
</mat-menu>
</div>
<div class="graph-tabs">
<mat-tab-group [(selectedIndex)]="graphService.selectedTab" (selectedTabChange)="tabChanged()">
<mat-tab *ngFor="let t of graphService.graphTabs">
<ng-template mat-tab-label>
{{t.title}}
<button mat-button *ngIf="t.title != 'ISO'" matSuffix mat-icon-button aria-label="Clear" (click)="graphService.removeTab(t)">
<mat-icon>close</mat-icon>
</button>
</ng-template>
<div class="coverage-box" *ngIf="!t.isIso"><a class="fake-link" (click)="filterIsoCoverage(t)">ISO:</a> {{t.coverage.coverage}}, <a class="fake-link" (click)="filterMapped(t)">Mapped:</a> {{t.coverage.mapped}}, <span>Unique:</span> {{t.coverage.uniqueconnections}}</div>
<!--<div class="coverage-box" *ngIf="t.isIso">-</div>-->
<div>
<mat-form-field class="filter-field">
<input matInput type="text" placeholder="Search..." [(ngModel)]="t.searchValue" #filter3 (keyup)="filter(filter3.value, tree.treeModel)">
<button mat-button *ngIf="t.searchValue" matSuffix mat-icon-button aria-label="Clear" (click)="filter('', tree.treeModel); t.searchValue=''">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</div>
<div>
<mat-checkbox *ngIf="t.anyExpanded" (change)="t.expandAll()" [checked]="true">Collapse All</mat-checkbox>
<mat-checkbox *ngIf="!t.anyExpanded" (change)="t.expandAll()" [checked]="false">Expand All</mat-checkbox>
<mat-checkbox *ngIf="t.anySelected" (change)="t.selectAll()" [checked]="true">Deselect All</mat-checkbox>
<mat-checkbox *ngIf="!t.anySelected" (change)="t.selectAll()" [checked]="false">Select All</mat-checkbox>
</div>
<div class="tree-container">
<tree-root #tree [(state)]="t.state" [options]="t.options" [nodes]="t.nodes" (initialized)="t.treeModel = tree.treeModel" (stateChange)="t.parentTabTreeChanged(updateSubject)">
<ng-template #treeNodeWrapperTemplate let-node let-index="index">
<div #wrapper class="node-wrapper" [style.padding-left]="node.getNodePadding()">
<tree-node-expander [node]="node"></tree-node-expander>
<tree-node-checkbox [node]="node"></tree-node-checkbox>
<div class="node-content-wrapper"
[class.dummystyle]="bindTogether(node, wrapper, svgbg)"
[class.node-content-wrapper-active]="node.isActive"
[class.node-content-wrapper-focused]="node.isFocused"
(click)="node.mouseAction('click', $event)"
[style.background-color]="getNodeColor(t, node)">
<span>
<span *ngIf="!node.data.node.hyperlink" [innerHTML]="injectHighlightSection(node.data)"></span>
<a *ngIf="node.data.node.hyperlink" (mousedown)="openTab(node.data.node.hyperlink)" class="fake-link" target="_blank" href="{{node.data.node.hyperlink}}" [innerHTML]="injectHighlightSection(node.data)"></a>
<span [innerHTML]="injectHighlightBody(node.data)"></span>
</span>
</div>
</div>
</ng-template>
</tree-root>
</div>
</mat-tab>
</mat-tab-group>
</div>
</mat-card>
</div>
<div fxFlex="1 0 auto">
<mat-card class="graph-columns" >
<!-- <div fxLayout="row" fxLayoutGap="80px" fxLayoutAlign="flex-start">-->
<div class="graph-column" *ngFor="let t of graphService.graphTabs" >
<h3>{{t.title}}</h3>
<!-- <div><a routerLink="/doctypes/{{t.title}}"><h3>{{t.title}}</h3></a>
<div>
<mat-checkbox (change)="t.column.expandAll()" [checked]="t.column.anyExpanded">{{t.column.anyExpanded ? 'Collapse' : 'Expand'}} All</mat-checkbox>
</div></div>-->
<tree-root #tree [(state)]="t.column.state" [options]="t.column.options" [nodes]="t.column.nodes" (initialized)="setup(t.column, tree); t.parentTabTreeChanged(updateSubject)" (activate)="activateNode(t, $event)" (deactivate)="t.columnTabTreeChanged(null, updateSubject)" (toggleExpanded)="t.columnTabTreeChanged($event, updateSubject)">
<ng-template #treeNodeWrapperTemplate let-node let-index="index">
<div #wrapper class="node-wrapper" [style.padding-left]="node.getNodePadding()">
<!--<tree-node-expander [node]="node"></tree-node-expander>-->
<div class="node-content-wrapper"
[class.dummystyle]="bindTogether(node, wrapper, svgbg)"
[class.node-content-wrapper-active]="node.isActive"
[class.node-content-wrapper-focused]="node.isFocused"
(click)="node.mouseAction('click', $event)"
[style.background-color]="getNodeColor(t.column, node)">
<span>
<span *ngIf="!node.data.node.hyperlink">{{node.data.node.section}}</span>
<a *ngIf="node.data.node.hyperlink" (mousedown)="openTab(node.data.node.hyperlink)" class="fake-link" target="_blank" href="{{node.data.node.hyperlink}}">{{node.data.node.section}}</a>
<span>{{node.data.node.body ? (' - ' + node.data.node.body) : ''}}</span>
</span>
</div>
</div>
</ng-template>
</tree-root>
</div>
<!-- </div>-->
<svg #svgbg class="svg-background">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#000" />
</marker>
</defs>
<g *ngFor="let t of graphService.graphTabs">
<g *ngFor="let l of t.column.displayLinks">
<!-- <line class="svg-line-click" (click)="clickedLink(l)" [attr.x1]="l.x3" [attr.x2]="l.x4" [attr.y1]="l.y2" [attr.y2]="l.y2" marker-end="url(#arrow)"></line>
<line class="svg-line-click" (click)="clickedLink(l)" [attr.x1]="l.x1" [attr.x2]="l.x3" [attr.y1]="l.y1" [attr.y2]="l.y2"></line>-->
<line class="svg-line-visible" [class.svg-line-active]="l.weight > 1" [attr.x1]="l.x3" [attr.x2]="l.x4" [attr.y1]="l.y2" [attr.y2]="l.y2" marker-end="url(#arrow)"></line>
<line class="svg-line-visible" [class.svg-line-active]="l.weight > 1" [attr.x1]="l.x1" [attr.x2]="l.x3" [attr.y1]="l.y1" [attr.y2]="l.y2"></line>
</g>
</g>
</svg>
<!-- <div class="table-wrapper" *ngIf="tableData">
</div>
<div id="d3" class="d3">
</div>-->
</mat-card>
</div>
</div>
</div>

Просмотреть файл

@ -0,0 +1,791 @@
import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
import * as d3 from 'd3';
import * as d3Sankey from 'd3-sankey';
import { DAG, SNode, GraphService, CategoryList, FilterCriteria, GraphTab } from '../graph.service';
import { FullDocNode } from '../standard-map';
import { DomSanitizer } from '@angular/platform-browser';
import { debounce } from 'rxjs/operators';
import * as Rx from 'rxjs';
import { TreeModel, TreeNode, ITreeState } from 'angular-tree-component';
import Fuse from 'fuse.js';
import convert from 'color-convert';
import selection_attrs from 'd3-selection-multi/src/selection/attrs';
d3.selection.prototype.attrs = selection_attrs;
class TableData
{
constructor(
public headers: string[],
public rows: SNode[][]) {
}
}
function saturateColor(input, saturationZeroToOne){
if (input == "unset")
return input;
var out = '#' + convert.keyword.hex(input) + saturationZeroToOne ;
return out;
}
@Component({
selector: 'app-d3-test',
templateUrl: './d3-test.component.html',
styleUrls: [ './d3-test.component.css' ],
encapsulation: ViewEncapsulation.None // Allow D3 to read styles through shadow DOM
})
export class D3TestComponent implements OnInit, OnDestroy {
public graphType: number = 0;
public graphData: DAG;
public graphCategories: CategoryList = [];
public graphCriteria = new FilterCriteria();
public tableData: TableData = null;
public complianceColors = ["white", "green", "yellow", "red", "black"];
public svgbgElement: any;
private updateSubject = new Rx.BehaviorSubject(0);
private updateViewSubject = new Rx.BehaviorSubject(0);
private searchSubject = new Rx.BehaviorSubject(null);
private tabsChangedSubscription;
constructor(
public graphService: GraphService,
private sanitizer: DomSanitizer) {
this.updateSubject.pipe(debounce(() => Rx.timer(1))).subscribe({
next: (v) => this.updateGraph()
});
this.updateViewSubject.pipe(debounce(() => Rx.timer(1))).subscribe({
next: (v) => this.updateGraphView()
});
this.searchSubject.pipe(debounce(() => Rx.timer(100))).subscribe({
next: (v) => {
if (v)
this.filterFn(v[0], v[1]);
}
});
};
ngOnInit(): void {
this.graphService.getDocTypes()
.subscribe(dt => {
this.graphCategories = dt;
//// activate first 3
//for (var i = 0; i< 3; ++i)
// this.graphCategories[i].active = true;
//this.RefreshGraph();
});
this.tabsChangedSubscription = this.graphService.tabsChangedSubject.subscribe(a => {
//this.tabChanged();
})
}
ngOnDestroy(): void {
this.tabsChangedSubscription.unsubscribe();
}
public getMenuOptions(): any[] {
var result = [];
if (this.graphService.canAdd)
{
for (var t of this.graphCategories)
if (!this.graphService.graphTabs.find(g => g.title == t.id))
result.push(t);
}
return result;
}
//public RefreshGraph() {
//
// var limitedDocs = this.graphCategories.filter(v => v.active).map(v => v.id);
// if (limitedDocs.length > 0)
// this.graphCriteria.categoryIds = limitedDocs;
//
// this.graphCriteria.categoryOrder = limitedDocs;
//
// if (this.graphType == 1)
// {
// // move ISO to second slot so it draws in the middle
// var i0 = this.graphCriteria.categoryOrder[0];
// var i1 = this.graphCriteria.categoryOrder[1];
// this.graphCriteria.categoryOrder[0] = i1;
// this.graphCriteria.categoryOrder[1] = i0;
// }
//
// this.graphService.getGraphData2(this.graphCriteria)
// .subscribe(gd => {
// console.log(JSON.stringify(gd));
// this.graphData = gd;
//
// switch (this.graphType)
// {
// case 0:
// this.DrawTable(this.graphData);
// break;
// case 1:
// this.DrawChart(this.graphData);
// break;
// case 2:
// this.DrawGraph(this.graphData);
// break;
// }
// });
//}
private DrawTable(data: DAG) {
var rowType = this.graphCriteria.categoryOrder ? this.graphCriteria.categoryOrder[0] : (data.nodes[0] as SNode).data.type;
var headerTypes = this.graphCriteria.categoryOrder.filter(v => v != rowType);
var headerNodes = [rowType].concat(headerTypes);
var rowNodes = data.nodes.filter(v => v.data.type == rowType).map(d => {
var links = headerTypes.map(h => {
for (var l of data.links)
{
var node = null;
if (l.source == d.nodeId && l.targetNode.data.type == h)
node = l.targetNode;
if (l.target == d.nodeId && l.sourceNode.data.type == h)
node = l.sourceNode;
if (node)
return node;
}
return null;
});
return [d].concat(links);
});
var width = 960;
var height = 500;
// clear
d3.selectAll("#d3").selectAll("*").remove();
this.tableData = new TableData(headerNodes, rowNodes);
}
private DrawChart(energy: DAG) {
var width = 960;
var height = 500;
// clear
d3.selectAll("#d3").selectAll("*").remove();
this.tableData = null;
var svg = d3.selectAll("#d3").append("svg");
svg.attr("width", width);
svg.attr("height", height);
var formatNumber = d3.format(",.0f"),
format = function (d: any) { return formatNumber(d) + " TWh"; },
color = d3.scaleOrdinal(d3.schemeCategory10);
var sankey = d3Sankey.sankey()
.nodeWidth(15)
.nodePadding(10)
.extent([[1, 1], [width - 1, height - 6]]);
var link = svg.append("g")
.attr("class", "links")
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.selectAll("path");
var node = svg.append("g")
.attr("class", "nodes")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g");
sankey.nodeAlign(d3Sankey.sankeyLeft);
//sankey.nodeAlign((n, d) => {
// return this.graphCriteria.categoryOrder.indexOf(n.data.type);
//});
sankey.nodeSort((a: SNode, b: SNode) => a.name < b.name ? -1 : 1);
sankey.nodeId((d: SNode) => d.nodeId);
sankey.nodeWidth(250);
sankey(energy);
link = link
.data(energy.links);
link.enter().append("path")
.attr("d", d3Sankey.sankeyLinkHorizontal())
.attr("stroke-width", function (d: any) { return 10; }) //Math.max(1, d.width) * 0.; })
.attr("stroke", d => "black") //color(d.source.name))
.on('click', function(d, i) {
console.log("clicked link", d);
})
.on("mouseover", function(d) {
d3.select(this).style("cursor", "pointer");
});
link.append("title")
.text(function (d: any) { return d.source.name + " → " + d.target.name + "\n" + format(d.value) + "\n" + d.uom; });
node = node
.data(energy.nodes)
.enter().append("g");
node.append("rect")
.attr("x", function (d: any) { return d.x0; })
.attr("y", function (d: any) { return d.y0; })
.attr("height", function (d: any) { return d.y1 - d.y0; })
.attr("width", function (d: any) { return d.x1 - d.x0; })
.attr("fill", d => { return this.complianceColors[energy.nodes[d.index].data.compliance_level]; } ) //color(d.name.replace(/ .*/, "")); })
.attr("stroke", "#000")
.on('click', function(d, i) {
console.log("clicked node", d);
})
.on("mouseover", function(d) {
d3.select(this).style("cursor", "pointer");
});
node.append("text")
.attr("x", function (d: any) { return d.x0 + 6; }) //{ return d.x0 - 6; })
.attr("y", function (d: any) { return (d.y1 + d.y0) / 2; })
.attr("dy", "0.35em")
.attr("text-anchor", "start")
.text(function (d: any) { return d.name + "\n" + d.data.body; });
//.filter(function (d: any) { return d.x0 < width / 2; })
//.attr("x", function (d: any) { return d.x1 + 6; })
//.attr("text-anchor", "start");
node.append("title")
.text(function (d: any) { return d.name + "\n" + d.data.body; });
}
private DrawGraph(data: DAG) {
var width = 960;
var height = 500;
// clear
d3.selectAll("#d3").selectAll("*").remove();
this.tableData = null;
var svg = d3.selectAll("#d3").append("svg");
svg.attr("width", width);
svg.attr("height", height);
var link, node, edgelabels, edgepaths;
var colors = d3.scaleOrdinal(d3.schemeCategory10);
svg.append('defs').append('marker')
.attrs({'id':'arrowhead',
'viewBox':'-0 -5 10 10',
'refX':13,
'refY':0,
'orient':'auto',
'markerWidth':13,
'markerHeight':13,
'xoverflow':'visible'})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke','none');
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {return d.nodeId;}).distance(100).strength(1))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
update(data.links, data.nodes);
function update(links, nodes) {
link = svg.selectAll(".link")
.data(links)
.enter()
.append("line")
.attr("class", "link")
.attr('marker-end','url(#arrowhead)');
link.append("title")
.text(function (d) {return ""; }); //d.type;});
edgepaths = svg.selectAll(".edgepath")
.data(links)
.enter()
.append('path')
.attrs({
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'id': function (d, i) {return 'edgepath' + i}
})
.style("pointer-events", "none");
edgelabels = svg.selectAll(".edgelabel")
.data(links)
.enter()
.append('text')
.style("pointer-events", "none")
.attrs({
'class': 'edgelabel',
'id': function (d, i) {return 'edgelabel' + i},
'font-size': 10,
'fill': '#aaa'
});
edgelabels.append('textPath')
.attr('xlink:href', function (d, i) {return '#edgepath' + i})
.style("text-anchor", "middle")
.style("pointer-events", "none")
.attr("startOffset", "50%")
.text(function (d) {return ""; });
node = svg.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
//.on("end", dragended)
);
node.append("circle")
.attr("r", 5)
.style("fill", function (d, i) {return colors(i);})
node.append("title")
.text(function (d) {return d.nodeId;});
node.append("text")
.attr("dy", -3)
.text(function (d) {return d.name;});
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
function ticked() {
link
.attr("x1", function (d) {return d.source.x;})
.attr("y1", function (d) {return d.source.y;})
.attr("x2", function (d) {return d.target.x;})
.attr("y2", function (d) {return d.target.y;});
node
.attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";});
edgepaths.attr('d', function (d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
});
edgelabels.attr('transform', function (d) {
if (d.target.x < d.source.x) {
var bbox = this.getBBox();
var rx = bbox.x + bbox.width / 2;
var ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
}
else {
return 'rotate(0)';
}
});
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
}
public filter(value: string, treeModel: TreeModel) {
this.searchSubject.next([value, treeModel]);
}
public filterFn(value: string, treeModel: TreeModel) {
if (value != "")
treeModel.filterNodes((node: TreeNode) => this.fuzzysearch(value, node));
else
treeModel.filterNodes((node: TreeNode) => this.clearSearch(node));
}
public tabChanged() {
this.graphService.configureFilterStack();
if (this.graphService.selectedTab >= 0 && this.graphService.selectedTab < this.graphService.graphTabs.length)
{
this.graphService.graphTabs[this.graphService.selectedTab].parentTabTreeChanged(this.updateSubject);
}
}
public activateNode(tab: GraphTab, event: any) {
if (tab.column.treeModel) {
var newSelection = {};
newSelection[tab.column.state.focusedNodeId] = true; // single select
tab.column.state.activeNodeIds = newSelection;
this.updateSubject.next(0);
}
}
public onResize(event) {
//event.target.innerWidth;
this.updateViewSubject.next(0);
}
private buildLinkSet(fromTab: GraphTab, toTab: GraphTab, rtl: boolean): void {
// For all the links that are not filtered already
var links = fromTab.visibleLinks;
// make a hash table from source _root_ node id, to list of links.
// The source _root_ node is the first node in the ascenstery that is visible (parent is not collpased)
var rollup = links.reduce((a, b) => {
var owner = b.fromNode;
// Iterate to root, keep track of highest collapsed node.
var iterator = owner;
while (iterator.realParent)
{
iterator = iterator.realParent;
if (iterator.isCollapsed)
owner = iterator;
}
// Create the list if it doesnt exist
if (!(owner.id in a))
a[owner.id] = [];
// add the link
a[owner.id].push(b);
return a;
}, { });
// for each source root node, aggregate links to destination root nodes
// destination _root_ nodes are the first node in the ascenstery that is visible (parent is not collpased)
var rollup2 = Object.keys(rollup).map(k => {
var collapsed = rollup[k].reduce((a, b) => {
var link = b.link;
var owner = toTab.treeModel.getNodeById(link.id);
if (!owner) {
toTab.parent.errors["- node not found: " + link.id + ", Referenced from: " + b.fromNode.id] = true;
return a;
}
// Iterate to root, keep track of highest collapsed node.
var iterator = owner;
while (iterator.realParent)
{
iterator = iterator.realParent;
if (iterator.isCollapsed)
owner = iterator;
}
// Create the list if it doesnt exist
if (!(owner.id in a))
a[owner.id] = [];
// add the link
a[owner.id].push(link);
return a;
}, { });
return [k, collapsed];
});
// Create one aggregated list of links with all the necessary parameters for the view
// start with the links map from source root node, to destination root nodes
var flatten = rollup2.reduce((a, b) => {
var fromTree = fromTab.treeModel;
var toTree = toTab.treeModel;
var destinationMap = b[1];
var fromNode = fromTree.getNodeById(b[0]);
// If the source node is hidden, continue.
if (fromNode.id in fromTree.hiddenNodeIds)
return a;
// one source node may map to many destination.
for (var destinationKey in destinationMap)
{
var toNode = toTree.getNodeById(destinationKey);
if (!(toNode.id in toTree.hiddenNodeIds))
{
var destinationData = destinationMap[destinationKey];
a.push({
from: b[0],
fromNode: fromNode,
to: destinationKey,
toNode: toNode,
fromTree: fromTree,
toTree: toTree,
rtl: rtl,
scale: rtl ? -1 : 1,
weight: (fromNode.isActive || toNode.isActive) ? 2 : 1,
x1: 0,
x2: 0,
x3: 0,
x4: 0,
y1: 0,
y2: 0,
});
}
}
return a;
}, []);
fromTab.displayLinks = flatten;
}
public updateGraph() {
var tabs = this.graphService.graphTabs;
// delay the rendering so dom can settle.
//setTimeout(a => {
var isoTab = tabs.find(t => t.isIso);
var isoIndex = tabs.indexOf(isoTab);
for (var t = 0; t < tabs.length; ++t)
{
var tab = tabs[t];
if (tab != isoTab)
this.buildLinkSet(tab.column, isoTab.column, t > isoIndex);
}
this.updateViewSubject.next(0);
//}, 1);
}
public updateGraphView() {
if (!this.svgbgElement)
return;
var tabs = this.graphService.graphTabs;
var startingGapLeft = 0;
var startingGapRight = 10;
var arrowLength = 10;
var svgBounds = this.svgbgElement.getBoundingClientRect();
for (var tab of tabs)
{
for (var l of tab.column.displayLinks)
{
var fromBounds = l.fromNode.elementRef2.getBoundingClientRect();
var toBounds = l.toNode.elementRef2.getBoundingClientRect();
l.x1 = (l.rtl ? (fromBounds.left - startingGapRight) : (fromBounds.right + startingGapLeft)) - svgBounds.left;
l.x2 = (l.rtl ? (toBounds.right + arrowLength) : (toBounds.left - arrowLength)) - svgBounds.left;
l.y1 = fromBounds.top - svgBounds.top + fromBounds.height * 0.5;
l.y2 = toBounds.top - svgBounds.top + toBounds.height * 0.5;
// Locations for the arrow head
l.x3 = l.x2 - 2 * l.scale;
l.x4 = l.x2 + 0.1 * l.scale;
}
}
}
public setup(data, treeElement) {
data.treeModel = treeElement.treeModel;
treeElement.viewportComponent.elementRef.nativeElement.addEventListener('scroll', t => this.updateGraphView());
}
public clickedLink(link: any) {
link.fromTree.getNodeById(link.from).expandAll();
link.toTree.getNodeById(link.to).expandAll();
}
public bindTogether(node, element, svgbg) {
node.elementRef2 = element;
this.svgbgElement = svgbg;
}
public clearSearch(node: TreeNode): boolean {
node.data.highlight = undefined;
return true;
}
public fuzzysearch(searchTerm: string, node: TreeNode): boolean {
var options = {
includeMatches: true,
includeScore: true,
  shouldSort: false,
  tokenize: false,
  threshold: 0.3,
  location: 0,
  distance: 800,
  maxPatternLength: 32,
  minMatchCharLength: 3,
  };
var result = [];
var scoreThresh = 0;
var inName = false;
// Test body first
if (node.data.node.body)
{
if (!node.data.bodyFuse)
{
node.data.bodyFuse = new Fuse([node.data.node.body], options);
}
result = node.data.bodyFuse.search(searchTerm);
result = result.filter(a => a.score > scoreThresh);
}
if (result.length == 0)
{
// test title
if (!node.data.sectionFuse)
{
node.data.sectionFuse = new Fuse([node.data.node.section], options);
}
result = node.data.sectionFuse.search(searchTerm);
result = result.filter(a => a.score > scoreThresh);
inName = true;
}
if (result.length > 0)
{
// record the longest matching span for highlight
var length = -1;
var longest = undefined;
var match = result[0].matches[0];
if (match)
{
for (var span of match.indices)
{
var newLength = span[1] - span[0];
if (newLength > length)
{
length = newLength;
longest = span;
span[1] += 1; // convert ending index to js substring convention of end + 1
}
}
}
node.data.highlight = longest;
node.data.highlightName = inName;
return true;
}
node.data.highlight = undefined;
return false;
}
private highlightText(text: string, highlight: number[]): string
{
return text.substring(0, highlight[0]) + "<mark>" + text.substring(highlight[0], highlight[1]) + "</mark>" + text.substring(highlight[1], text.length);
}
public injectHighlightSection(data: FullDocNode)
{
var section = data.node.section;
if (data.highlight && data.highlightName)
section = this.highlightText(section, data.highlight);
return this.sanitizer.bypassSecurityTrustHtml(section);
}
public injectHighlightBody(data: FullDocNode)
{
var body = data.node.body;
if (body)
{
if (data.highlight && !data.highlightName)
{
body = this.highlightText(body, data.highlight);
}
body = " - " + body;
}
else
body = "";
return this.sanitizer.bypassSecurityTrustHtml(body);
}
public openTab(url: string)
{
window.open(url, "_blank").focus();
}
public getNodeColor(tab: GraphTab, node: TreeNode)
{
// if we're a tree in the right side view, highlight active nodes
var selected = (tab.parent && node.isActive);
var color = selected ? 'lightblue' : 'unset';
if (tab.parent && node.data.filterColor)
{
if (!tab.isIso && node.data.isUnmapped)
{
color = 'red';
}
else if (!selected)
{
color = node.data.filterColor;
}
}
else if (!tab.isIso)
{
// Iso never has outward mappings
if (node.data.isUnmapped)
{
color = 'red';
}
else if (node.data.isAnyChildUnmapped)
{
color = 'pink';
}
}
// if we're a tree in the right side view
if (tab.parent && !node.isActive)
{
// desaturate background color unless active node
color = saturateColor(color, 'A0');
}
return color;
}
public filterMapped(tab: GraphTab)
{
tab.filterMapped();
this.graphService.activateTab(tab);
}
public filterIsoCoverage(tab: GraphTab)
{
var isoTab = this.graphService.graphTabs[1];
isoTab.filterToIds(tab.coverage.uncoveredIds);
this.graphService.activateTab(isoTab);
}
}

Просмотреть файл

@ -0,0 +1,2 @@
/* DashboardComponent's private CSS styles */

Просмотреть файл

@ -0,0 +1,13 @@

<app-d3-test></app-d3-test>
<div *ngFor="let t of graphService.graphTabs">
<div *ngIf="t.anyErrors">
{{t.title}} Errors
<div *ngFor="let e of t.errorStrings">
<div>
<h4>{{e}}</h4>
</div>
</div>
</div>
</div>

Просмотреть файл

@ -0,0 +1,58 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
import { StandardMapSearchComponent } from '../standard-map-search/standard-map-search.component';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { STANDARDMAPS } from '../mock-standard-map';
import { StandardMapService } from '../standard-map.service';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
let standardMapService;
let getStandardMapsSpy;
beforeEach(async(() => {
standardMapService = jasmine.createSpyObj('StandardMapService', ['getStandardMaps']);
getStandardMapsSpy = standardMapService.getStandardMaps.and.returnValue( of(STANDARDMAPS) );
TestBed.configureTestingModule({
declarations: [
DashboardComponent,
StandardMapSearchComponent
],
imports: [
RouterTestingModule.withRoutes([])
],
providers: [
{ provide: StandardMapService, useValue: standardMapService }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should display "Top Standard Maps" as headline', () => {
expect(fixture.nativeElement.querySelector('h3').textContent).toEqual('Top Standard Maps');
});
it('should call standardMapService', async(() => {
expect(getStandardMapsSpy.calls.any()).toBe(true);
}));
it('should display 4 links', async(() => {
expect(fixture.nativeElement.querySelectorAll('a').length).toEqual(4);
}));
});

Просмотреть файл

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { GraphService } from '../graph.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
constructor(
public graphService: GraphService) { }
ngOnInit() {
}
}

Двоичные данные
src/app/data/Database.xlsx Normal file

Двоичный файл не отображается.

11462
src/app/data/sampledb.json Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

733
src/app/graph.service.ts Normal file
Просмотреть файл

@ -0,0 +1,733 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of, forkJoin } from 'rxjs';
import * as Rx from 'rxjs';
import { catchError, map, tap, debounce } from 'rxjs/operators';
import { FullDocNode, DocNode2, Doc2, Link } from './standard-map';
import { MessageService } from './message.service';
import * as d3Sankey from 'd3-sankey';
import { TreeModel, TreeNode, ITreeState, TREE_ACTIONS, IActionMapping } from 'angular-tree-component';
export interface ICategory {
id: string;
title: string;
active?: boolean;
}
export type CategoryList = ICategory[];
export class FilterCriteria {
constructor(
public categoryIds: string[] = null,
public categoryOrder: string[] = null) {
}
}
// -- Dag Node --
export interface SNodeExtra {
nodeId: number;
name: string;
data?: any;
}
export interface SLinkExtra {
source: number;
target: number;
value: number;
uom: string;
sourceNode: any;
targetNode: any;
}
export type SNode = d3Sankey.SankeyNode<SNodeExtra, SLinkExtra>;
export type SLink = d3Sankey.SankeyLink<SNodeExtra, SLinkExtra>;
export interface DAG {
nodes: SNode[];
links: SLink[];
}
// -- Dag Node --
export class VisibleLink {
constructor(
public fromNode: TreeNode,
public link: Link) {
}
}
export class GraphTab {
public options = {
useCheckbox: true,
allowDrag: false,
allowDrop: false,
scrollOnActivate: false,
scrollContainer: <HTMLElement>document.body, // Fix for bug: https://github.com/500tech/angular-tree-component/issues/704
actionMapping: {
mouse: {
click: (tree, node, $event) => {
TREE_ACTIONS.TOGGLE_ACTIVE_MULTI(tree, node, $event);
},
}
}
};
public state: ITreeState = { };
public treeModel: TreeModel;
public visibleNodes: TreeNode[] = [];
public visibleLinks: VisibleLink[] = [];
public displayLinks: any[] = [];
public isIso: boolean = false;
public searchValue: string = null;
public coverage: any = null;
public autoFilterSrc: GraphTab;
public autoFilterSelf: boolean;
public autoFilterParent: GraphTab;
public errors: any = {};
private updateSubjectParent = new Rx.BehaviorSubject(null);
private updateSubjectColumn = new Rx.BehaviorSubject(null);
constructor(
public title: string,
public graphService: GraphService,
public parent: GraphTab = null) {
this.isIso = title == "ISO";
if (!parent)
{
this.column = new GraphTab(title, this.graphService, this);
this.column.options.useCheckbox = false;
this.updateSubjectParent.pipe(debounce(() => Rx.timer(1))).subscribe({
next: (v) => this.parentTabTreeChangedImp(v)
});
this.updateSubjectColumn.pipe(debounce(() => Rx.timer(1))).subscribe({
next: (v) => this.columnTabTreeChangedImp(v)
});
}
}
public get errorStrings(): string[] {
return Object.keys(this.errors);
}
public get anyErrors(): boolean {
return this.errorStrings.length > 0;
}
public get anyExpanded():boolean {
return this.treeModel && this.treeModel.expandedNodes.length > 0;
}
public get anySelected():boolean {
return this.treeModel && this.treeModel.selectedLeafNodeIds && Object.keys(this.treeModel.selectedLeafNodeIds).length > 0;
}
public nodes = [];
public column: GraphTab;
public static filterBySelectedLeafs(visibleNodes: TreeNode[], parentTree: TreeModel, node: TreeNode, noSelection: boolean): boolean
{
var inSelection = node.data.id in parentTree.selectedLeafNodeIds;
node.data.filterColor = inSelection ? "yellow" : undefined;
var show = noSelection || inSelection;
if (show)
visibleNodes.push(node);
return show;
}
public static containsNode(parentTree: VisibleLink[], node: TreeNode): boolean
{
return parentTree.find(n => {
return n.link.id == node.data.id;
}) != null
}
public static filterByVisibleLinks(visibleNodes: TreeNode[], parentTree: VisibleLink[], node: TreeNode): boolean
{
var show = GraphTab.containsNode(parentTree, node);
var parent = node.parent;
while (!show && parent)
{
// didnt find us but see if any parent is linked
show = GraphTab.containsNode(parentTree, parent);
parent = parent.parent;
}
if (show)
visibleNodes.push(node);
return show;
}
public static filterByMyLinks(visibleNodes: TreeNode[], parentTree: GraphTab, node: TreeNode): boolean
{
// keep unmapped stuff by default
var show = node.data.isUnmapped;
if (!show)
{
// include linked stuff
var links = node.data.node.links;
if (links)
{
for (var l of links)
{
if (!(l.id in parentTree.state.hiddenNodeIds) || !parentTree.state.hiddenNodeIds[l.id])
{
show = true;
break;
}
}
}
}
if (show)
visibleNodes.push(node);
return show;
}
public runFilter()
{
this.visibleNodes = [];
this.visibleLinks = [];
if (this.treeModel)
{
this.treeModel.clearFilter();
var noSelection = Object.keys(this.parent.treeModel.selectedLeafNodeIds).length == 0;
this.treeModel.filterNodes((node: TreeNode) => {
var show = false;
if (this.autoFilterSrc)
{
if (this.autoFilterSelf)
{
show = GraphTab.filterByMyLinks(this.visibleNodes, this.autoFilterSrc, node);
}
else
show = GraphTab.filterByVisibleLinks(this.visibleNodes, this.autoFilterSrc.visibleLinks, node);
}
else
{
show = GraphTab.filterBySelectedLeafs(this.visibleNodes, this.parent.treeModel, node, noSelection);
}
return show;
}, false);
if (this.autoFilterParent)
{
if (this.autoFilterParent.anySelected)
this.treeModel.expandAll();
else
this.treeModel.collapseAll();
}
this.visibleLinks = GraphService.flatten(this.visibleNodes.map(v => {
var links = v.data.node.links;
return links ? links.map(l => new VisibleLink(v, l)) : [];
}));
}
}
public filterMapped()
{
// collapse all
this.forAllTreeNodes(n => n.collapse());
this.treeModel.selectedLeafNodeIds = { };
var scrolledOnce = false;
this.forAllTreeNodes(n => {
if (n.level > 0)
{
n.setIsSelected(n.data.isUnmapped);
if (n.data.isUnmapped)
{
n.ensureVisible();
if (!scrolledOnce)
{
scrolledOnce = true;
n.scrollIntoView();
}
}
}
});
}
public filterToIds(ids: string[])
{
// collapse all
this.forAllTreeNodes(n => n.collapse());
this.treeModel.selectedLeafNodeIds = { };
var scrolledOnce = false;
this.forAllTreeNodes(n => {
if (n.level > 0)
{
var select = ids.includes(n.id);
n.setIsSelected(select);
if (select)
{
n.ensureVisible();
if (!scrolledOnce)
{
scrolledOnce = true;
n.scrollIntoView();
}
}
}
});
}
public expandAll() {
if (this.anyExpanded)
{
this.treeModel.collapseAll();
}
else
{
this.treeModel.expandAll();
}
}
public selectAll() {
if (this.anySelected)
{
this.treeModel.selectedLeafNodeIds = { };
}
else
{
this.treeModel.selectedLeafNodeIds = { };
this.forAllTreeNodes(n => {
if (n.level > 0)
n.setIsSelected(true);
});
}
}
public mergedOptions(opts) {
return Object.assign(this.options, opts);
}
// Due to a bug in the tree control (forall is async but does not return the promise)
// create our own synchronous forall
public forAllTreeNodes(cb: (tn: TreeNode) => void)
{
for (var n of this.treeModel.roots)
this.forAllTreeNodesRecursive(n, cb);
}
private forAllTreeNodesRecursive(node: TreeNode, cb: (tn: TreeNode) => void)
{
cb(node);
if (node.children)
{
for (var n of node.children)
this.forAllTreeNodesRecursive(n, cb);
}
}
public parentTabTreeChanged(updateSubject: any) {
this.updateSubjectParent.next([updateSubject]);
}
public parentTabTreeChangedImp(data: any) {
if (!data)
return;
var updateSubject = data[0];
if (this.column.treeModel) {
// Due to a bug https://github.com/500tech/angular-tree-component/issues/521
// must manually clear nodes that are no longer selected
for (var n of Object.keys(this.treeModel.selectedLeafNodeIds))
{
var node = this.treeModel.getNodeById(n);
if (node && !node.isSelected)
delete this.treeModel.selectedLeafNodeIds[n];
}
this.graphService.runFilters(this, true, updateSubject);
updateSubject.next(0);
// Must be delayed or you'll get an infinite loop of change events.
//setTimeout(() => {
// by default, collapse everything
this.column.forAllTreeNodes(n => n.collapse());
// ensure selected nodes are visible
for (var n in this.treeModel.selectedLeafNodeIds)
{
var columnNode = this.column.treeModel.getNodeById(n);
if (columnNode)
columnNode.ensureVisible();
}
//}, 1);
}
}
public columnTabTreeChanged(event: any, updateSubject: any) {
this.updateSubjectColumn.next([event, updateSubject]);
}
public columnTabTreeChangedImp(data: any) {
if (!data)
return;
var event = data[0];
var updateSubject = data[1];
if (this.column.treeModel) {
if (event)
{
// on expand
if (event.isExpanded && event.node.isActive)
{
var allNodes = this.graphService.getNodesWithLinks(event.node.data.children, []);
// select children with links
for (var c of allNodes)
{
this.column.state.activeNodeIds[c.id] = true; // select child
}
}
}
this.graphService.runFilters(this, false, updateSubject);
updateSubject.next(0);
}
}
}
@Injectable({ providedIn: 'root' })
export class GraphService {
private docGuids = {};
private docDb = null;
private nextDocGuid = 0;
public tabsChangedSubject = new Rx.BehaviorSubject(0);
constructor(
private messageService: MessageService,
private http: HttpClient) {
this.addTab("ISO");
}
public static flatten(array) {
return array.reduce((a,b)=>a.concat(b), []);
}
public runFilters(changedTab: GraphTab, parentChanged: boolean, updateSubject: any)
{
var tabs = this.graphTabs;
for (var t of tabs)
{
if (t.column.autoFilterSrc == changedTab.column || (parentChanged && t == changedTab))
{
// filter child tree
t.column.runFilter();
t.columnTabTreeChanged(null, updateSubject);
}
}
}
getGuid(id: string, type: string, rev: string, createMissing: boolean = true): number {
var key = `${type}-${id}-${rev}`;
if (key in this.docGuids)
return this.docGuids[key];
if (!createMissing)
return null;
var value = this.nextDocGuid++;
this.docGuids[key] = value;
return value;
}
getDb() : Observable<Doc2[]> {
if (this.docDb)
return of(this.docDb);
return this.http.get<Doc2[]>('assets/db.json', {responseType: 'json'})
.pipe(
tap(
data => this.docDb = data,
error => this.handleError("getDb", [])
)
);
}
getDocTypes() : Observable<CategoryList> {
return this.getDb().pipe(
map(
data => {
return data.map(v => { return { id: v.type, title: v.type }; });
}
)
);
}
private addToDoc(parent: FullDocNode, input: DocNode2) {
var child = new FullDocNode(input);
if (parent)
{
parent.children.push(child);
}
// Recurse
for (var c of input.children)
{
this.addToDoc(child, c);
}
return child;
}
getFullDocByType(docType: string) : Observable<FullDocNode> {
return this.getDb().pipe(
map(
data => {
var doc = data.find(n => n.type == docType);
return this.addToDoc(null, doc);
}
)
);
}
// Live state management: maybe move this to a different service.
public graphTabs: GraphTab[] = [ ];
public selectedTab: number = 0;
public get canAdd(): boolean {
return this.graphTabs.length < 3;
}
public addTab(id: string) {
if (!this.canAdd)
return;
this.getFullDocByType(id)
.subscribe(doc => {
var newTab = new GraphTab(id, this);
newTab.nodes = doc.children;
newTab.column.nodes = doc.children;
this.graphTabs.push(newTab);
this.ensureISOIsInMiddle();
if (id != "ISO")
{
// compare with iso.
newTab.coverage = this.compareDocs(newTab.column, this.graphTabs[1]);
}
this.selectedTab = -1; // set it to non-value so change is detected if the index is the same
setTimeout(() => this.activateTab(newTab), 1); // need to let dom regenerate
this.tabsChangedSubject.next(0);
});
}
private ensureISOIsInMiddle() {
var isoTab = this.graphTabs.find(t => t.title == "ISO");
if (this.graphTabs.length > 1)
{
this.graphTabs = this.graphTabs.filter(t => t != isoTab);
this.graphTabs.splice(1, 0, isoTab);
}
}
public configureFilterStack() {
var filterOrder = [];
switch (this.selectedTab)
{
case 0: filterOrder = [0, 1, 2]; break;
case 1: filterOrder = [1, 0, 2]; break;
case 2: filterOrder = [2, 1, 0]; break;
}
// setup filters
var isoTab = this.graphTabs.find(t => t.title == "ISO");
var primary = this.graphTabs[filterOrder[0]];
if (!primary)
return;
// clear auto filter of left tab
primary.column.autoFilterSrc = null;
primary.column.autoFilterParent = null;
primary.column.autoFilterSelf = false;
var secondary = this.graphTabs[filterOrder[1]];
if (secondary)
{
if (secondary == isoTab)
{
// assure iso filters from the primary: "auto filter"
isoTab.column.autoFilterSrc = primary.column;
isoTab.column.autoFilterParent = primary.column.parent;
isoTab.column.autoFilterSelf = false;
}
else
{
// auto filter with this tabs connections to iso
secondary.column.autoFilterSrc = isoTab.column;
secondary.column.autoFilterParent = primary.column.parent; // the primary tab always drives the selection
secondary.column.autoFilterSelf = true;
}
}
var third = this.graphTabs[filterOrder[2]];
if (third)
{
// auto filter with this tabs connections to iso
third.column.autoFilterSrc = isoTab.column;
third.column.autoFilterParent = primary.column.parent; // the primary tab always drives the selection
third.column.autoFilterSelf = true;
}
}
public removeTab(tab) {
this.graphTabs = this.graphTabs.filter(t => t!=tab);
this.ensureISOIsInMiddle();
this.activateTab(this.graphTabs[0]);
}
public activateTab(tab: GraphTab) {
var newIndex = this.graphTabs.indexOf(tab);
if (newIndex != this.selectedTab)
{
this.selectedTab = newIndex;
this.configureFilterStack();
this.tabsChangedSubject.next(0);
}
else
{
this.selectedTab = -1;
setTimeout(() => {
this.selectedTab = newIndex;
this.configureFilterStack();
this.tabsChangedSubject.next(0);
}, 10);
}
}
public getNodesWithLinks(children: FullDocNode[], result: FullDocNode[])
{
for (var c of children)
{
if (c.node.links && c.node.links.length > 0)
result.push(c);
this.getNodesWithLinks(c.children, result);
}
return result;
}
public flattenSections(children: FullDocNode[], result: string[])
{
for (var c of children)
{
if (c.node.body)
result.push(c.id);
this.flattenSections(c.children, result);
}
return result;
}
public flattenLinks(children: FullDocNode[], result: Link[], linkData: any)
{
for (var c of children)
{
if (c.shouldBeMapped)
{
linkData.total++;
if (!c.isUnmapped)
{
linkData.linked++;
result = result.concat(c.node.links);
}
}
result = this.flattenLinks(c.children, result, linkData);
}
return result;
}
public compareDocs(aTab: GraphTab, bTab: GraphTab): any {
var bSections = [];
this.flattenSections(bTab.nodes, bSections);
var bCopy = bSections.slice();
var linkData = { total: 0, linked: 0 };
var aLinks = this.flattenLinks(aTab.nodes, [], linkData);
var found = 0;
var checked = 0;
for (var a of aLinks)
{
++checked;
var b = bCopy.find(x => x == a.id)
if (b)
{
bCopy = bCopy.filter(x => x != b);
++found;
}
}
return {
coverage: found + "/" + bSections.length,
mapped: linkData.linked + "/" + linkData.total,
uniqueconnections: found + "/" + checked,
uncoveredIds: bCopy
//"coverage": (found / bSections.length * 100).toFixed(1) + "% (" + found + "/" + bSections.length + ")",
//"mapped": (linkData.linked / linkData.total * 100).toFixed(1) + "% (" + linkData.linked + "/" + linkData.total + ")",
//"uniqueconnections": (found / checked * 100).toFixed(1) + "% (" + found + "/" + checked + ")"
};
}
/**
* Handle Http operation that failed.
* Let the app continue.
* @param operation - name of the operation that failed
* @param result - optional value to return as the observable result
*/
private handleError<T> (operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// TODO: better job of transforming error for user consumption
this.log(`${operation} failed: ${error.message}`);
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
/** Log a GraphService message with the MessageService */
private log(message: string) {
this.messageService.add(`GraphService: ${message}`);
}
}

Просмотреть файл

@ -0,0 +1,22 @@
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Injectable } from '@angular/core';
import { mapDb } from './mock-standard-maps'
@Injectable({
providedIn: 'root',
})
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const standardMaps = mapDb.slice();
return {'standard-map':standardMaps};
}
//// Overrides the genId method to ensure that a standard-map always has an id.
//// If the standardMaps array is empty,
//// the method below returns the initial number (11).
//// if the standardMaps array is not empty, the method below returns the highest
//// standard-map id + 1.
//genId(standardMaps: StandardMap[]): number {
// return standardMaps.length > 0 ? Math.max(...standardMaps.map(standardMap => standardMap.id)) + 1 : 11;
//}
}

Просмотреть файл

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { MessageService } from './message.service';
describe('MessageService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MessageService]
});
});
it('should be created', inject([MessageService], (service: MessageService) => {
expect(service).toBeTruthy();
}));
});

Просмотреть файл

@ -0,0 +1,14 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}

Просмотреть файл

@ -0,0 +1,35 @@
/* MessagesComponent's private CSS styles */
h2 {
color: red;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body, input[text], button {
color: crimson;
font-family: Cambria, Georgia;
}
button.clear {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #aaa;
cursor: auto;
}
button.clear {
color: #888;
margin-bottom: 12px;
}

Просмотреть файл

@ -0,0 +1,8 @@
<div *ngIf="messageService.messages.length">
<h2>Messages</h2>
<button class="clear"
(click)="messageService.clear()">clear</button>
<div *ngFor='let message of messageService.messages'> {{message}} </div>
</div>

Просмотреть файл

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MessagesComponent } from './messages.component';
describe('MessagesComponent', () => {
let component: MessagesComponent;
let fixture: ComponentFixture<MessagesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MessagesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MessagesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

Просмотреть файл

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { MessageService } from '../message.service';
@Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
constructor(public messageService: MessageService) {}
ngOnInit() {
}
}

Просмотреть файл

@ -0,0 +1,82 @@
import { DocNode2, Doc2 } from './standard-map';
//import * as mockMapDb from './data/msftgdprsample.json'
import * as mockMapDb from './data/sampledb.json'
interface SampleModule {
default: Doc2[]
};
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
var mockText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel velit magna. Ut ex est, consequat ultricies aliquam in, rutrum quis tortor. Etiam ex turpis, lacinia ac blandit eget, ultrices ac urna. Morbi pulvinar, leo vel gravida lacinia, velit dolor iaculis est, at porta arcu leo tempus lacus. Proin tincidunt, urna non varius mollis, turpis nisl hendrerit felis, nec scelerisque dui velit a tellus. Praesent consequat ante lacus, ac euismod libero elementum lacinia. Ut libero turpis, interdum id quam quis, malesuada dapibus tortor. Nulla ut orci eu odio pretium gravida et et nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Curabitur lorem magna, malesuada in placerat scelerisque, lacinia sit amet dui. Vestibulum non elementum dui. Proin et pellentesque nisl, efficitur congue tortor. Cras dapibus vel libero vel efficitur. Duis vitae diam quam. Vivamus ornare ullamcorper dolor, at facilisis est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.";
function GenerateStandardMapRecursive(doc: Doc2, result: DocNode2, sections: number, depth: number): void {
if (depth > 0)
{
for (var i = 1; i <= sections; ++i)
{
var body = null;
if (depth == 1)
{
var textLen = getRandomInt(40);
var textStart = getRandomInt(mockText.length - textLen);
body = mockText.substr(textStart, textLen);
}
var newId = i.toString();
// prepend parent
if (result.id)
newId = result.id + "." + newId;
var child = {
id: newId,
section: newId,
body: body,
compliance_level: 1,
children: [],
links: []
};
if (body && doc.type != "ISO")
{
// Link to iso doc
child.links.push({
id: newId,
type: "ISO",
rev: "1"
});
}
result.children.push(child);
GenerateStandardMapRecursive(doc, child, sections / 2, depth - 1);
}
}
}
function GenerateStandardMap(type: string, rev: string, sections: number, depth: number): Doc2 {
var result = {
type: type,
rev: rev,
children: [],
links: []
};
GenerateStandardMapRecursive(result, result, sections, depth);
return result;
}
export var mapDb: Doc2[] = (mockMapDb as any as SampleModule).default;
//export var mapDb2: Doc2[] = [
// GenerateStandardMap('ISO', "1", 8, 3),
// GenerateStandardMap('PIPEDA', "1", 8, 3),
// GenerateStandardMap('APP', "1", 8, 3)
//];

Просмотреть файл

@ -0,0 +1,57 @@
/* StandardMapDetailComponent's private CSS styles */
.container {
padding-left: 20px;
}
label {
display: inline-block;
width: 3em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
input {
height: 2em;
font-size: 1em;
padding-left: .4em;
}
button {
margin-top: 20px;
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer; cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #ccc;
cursor: auto;
}
.link {
padding-left: 20px;
}
.input-field {
display: block;
width: 100%;
}
.link-field {
display: inline-block;
}
mat-icon {
height: 5px;
font-size: 14px;
}
.example-spacer {
flex: 1 1 auto;
}

Просмотреть файл

@ -0,0 +1,68 @@
<div *ngIf="standardMap" class="container">
<button (click)="goBack()">Go back</button>
<h2>{{standardMap.name | uppercase}}</h2>
<div><mat-checkbox (change)="treeData.expandAll()" [checked]="treeData.anyExpanded">Expand All</mat-checkbox></div>
<tree-root #tree [focused]="true" [(state)]="treeData.state" [options]="treeData.options" [nodes]="treeData.nodes" (initialized)="treeData.treeModel = tree.treeModel;" >
<ng-template #treeNodeWrapperTemplate let-node let-index="index">
<div #wrapper class="node-wrapper" [style.padding-left]="node.getNodePadding()">
<tree-node-expander [node]="node"></tree-node-expander>
<div class="node-content-wrapper"
[class.node-content-wrapper-focused]="node.isFocused"
(click)="node.mouseAction('click', $event)"
(dblclick)="node.mouseAction('dblClick', $event)"
(contextmenu)="node.mouseAction('contextMenu', $event)"
(treeDrop)="node.onDrop($event)"
[treeAllowDrop]="node.allowDrop"
[treeDrag]="node"
[treeDragEnabled]="node.allowDrag()">
<div class="node-content">
<div *ngIf="(node.isExpanded || node.isLeaf)">
<div class="button-row">
<span class="example-spacer"></span>
<button><mat-icon>arrow_upward</mat-icon></button>
<button><mat-icon>arrow_downward</mat-icon></button>
<button><mat-icon>cancel</mat-icon></button>
</div>
<mat-form-field class="input-field">
<input matInput type="text" placeholder="id" [(ngModel)]="node.data.node.id" >
</mat-form-field>
<mat-form-field class="input-field">
<input matInput type="text" placeholder="section" [(ngModel)]="node.data.node.section" >
</mat-form-field>
<mat-form-field class="input-field">
<input matInput type="text" placeholder="body" [(ngModel)]="node.data.node.body" >
</mat-form-field>
<!--<tree-node-content [node]="node" [index]="index"></tree-node-content>-->
<h4>
<span>Links</span>
<span class="example-spacer"></span>
<button><mat-icon>add_box</mat-icon></button>
</h4>
<div *ngFor="let link of node.data.node.links">
<div class="link">
<mat-form-field class="link-field">
<input matInput type="text" placeholder="type" [(ngModel)]="link.type" >
</mat-form-field>
<mat-form-field class="link-field">
<input matInput type="text" placeholder="id" [(ngModel)]="link.id" >
</mat-form-field>
<mat-form-field class="link-field">
<input matInput type="text" placeholder="rev" [(ngModel)]="link.rev" >
</mat-form-field>
<button><mat-icon>cancel</mat-icon></button>
</div>
</div>
<h4 *ngIf="node.children.length">Children</h4>
</div>
<div *ngIf="!(node.isExpanded || node.isLeaf)">
<div>{{node.data.name}}</div>
</div>
</div>
</div>
</div>
</ng-template>
</tree-root>
</div>

Просмотреть файл

@ -0,0 +1,47 @@
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
import { FullDocNode } from '../standard-map';
import { GraphService, GraphTab } from '../graph.service';
@Component({
selector: 'app-standard-map-detail',
templateUrl: './standard-map-detail.component.html',
styleUrls: [ './standard-map-detail.component.css' ]
})
export class StandardMapDetailComponent implements OnInit {
@Input() standardMap: FullDocNode;
public treeData: GraphTab;
constructor(
private route: ActivatedRoute,
private graphService: GraphService,
private location: Location
) {}
ngOnInit(): void {
if (this.treeData == null)
{
// if it's not already injected, then get it from the url query
this.treeData = new GraphTab("test", this.graphService);
this.route.params.subscribe(params => {
this.graphService.getFullDocByType(params['id'])
.subscribe(standardMap => {
this.standardMap = standardMap;
this.treeData.nodes = standardMap.children;
});
});
}
}
goBack(): void {
this.location.back();
}
//save(): void {
// this.standardMapService.updateStandardMap(this.standardMap)
// .subscribe(() => this.goBack());
// }
}

Просмотреть файл

@ -0,0 +1,39 @@
/* standardMapSearch private styles */
.search-result li {
border-bottom: 1px solid gray;
border-left: 1px solid gray;
border-right: 1px solid gray;
width: 195px;
height: 16px;
padding: 5px;
background-color: white;
cursor: pointer;
list-style-type: none;
}
.search-result li:hover {
background-color: #607D8B;
}
.search-result li a {
color: #888;
display: block;
text-decoration: none;
}
.search-result li a:hover {
color: white;
}
.search-result li a:active {
color: white;
}
#search-box {
width: 200px;
height: 20px;
}
ul.search-result {
margin-top: 0;
padding-left: 0;
}

Просмотреть файл

@ -0,0 +1,11 @@
<div id="search-component">
<span>Search:</span><input #searchBox id="search-box" (input)="search(searchBox.value)" />
<ul class="search-result">
<li *ngFor="let standardMap of standardMap$ | async" >
<a routerLink="/detail/{{standardMap.id}}">
{{standardMap.name}}
</a>
</li>
</ul>
</div>

Просмотреть файл

@ -0,0 +1,29 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { StandardMapSearchComponent } from './standard-map-search.component';
describe('StandardMapSearchComponent', () => {
let component: StandardMapSearchComponent;
let fixture: ComponentFixture<StandardMapSearchComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StandardMapSearchComponent ],
imports: [RouterTestingModule.withRoutes([]), HttpClientTestingModule]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StandardMapSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Просмотреть файл

@ -0,0 +1,38 @@
import { Component, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import {
debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators';
@Component({
selector: 'app-standard-map-search',
templateUrl: './standard-map-search.component.html',
styleUrls: [ './standard-map-search.component.css' ]
})
export class StandardMapSearchComponent implements OnInit {
standardMap$: Observable<any[]>;
private searchTerms = new Subject<string>();
constructor() {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
//this.standardMap$ = this.searchTerms.pipe(
// // wait 300ms after each keystroke before considering the term
// debounceTime(300),
//
// // ignore new term if same as previous term
// distinctUntilChanged(),
//
// // switch to new search observable each time the term changes
// //switchMap((term: string) => this.standardMapService.searchStandardMaps(term)),
//);
}
}

88
src/app/standard-map.ts Normal file
Просмотреть файл

@ -0,0 +1,88 @@

export class Link {
id: string;
type: string;
rev: string;
}
export class DocNode2 {
id?: string;
section?: string;
body?: string;
hyperlink?: string;
compliance_level?: number;
children: DocNode2[];
links: Link[];
}
export class Doc2 extends DocNode2 {
type: string;
rev?: string;
}
export class FullDocNode {
public highlight: number[];
public highlightName: boolean;
public bodyFuse: any;
public sectionFuse: any;
public isUnmappedCached: boolean;
public isAnyChildUnmappedCached: boolean;
public shouldBeMappedCached: boolean;
public filterColor: string;
public constructor(
public node: Doc2 | DocNode2,
public children: FullDocNode[] = []) {
}
get name():string {
var name = this.node.section ? this.node.section : (this.node as Doc2).type;
if (this.node.body)
name += " - " + this.node.body;
return name;
}
get id(): string {
return this.node.id;
}
get isAnyChildUnmapped(): boolean {
if (this.isAnyChildUnmappedCached === undefined)
{
this.isAnyChildUnmappedCached = false;
for (var c of this.children)
{
if (c.isUnmapped || c.isAnyChildUnmapped)
{
this.isAnyChildUnmappedCached = true;
break;
}
}
}
return this.isAnyChildUnmappedCached;
}
get isUnmapped(): boolean {
if (this.isUnmappedCached === undefined)
{
// to be qualified as unmapped
this.isUnmappedCached = this.shouldBeMapped
&& (!this.node.links || this.node.links.length == 0); // no links
}
return this.isUnmappedCached;
}
get shouldBeMapped(): boolean {
if (this.shouldBeMappedCached === undefined)
{
// to be qualified as unmapped
this.shouldBeMappedCached = this.node.body // needs a body
&& (!this.node.children || this.node.children.length == 0); // no children
}
return this.shouldBeMappedCached;
}
}

Просмотреть файл

@ -0,0 +1,74 @@
/* StandardMapsComponent's private CSS styles */
.standard-maps {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.standard-maps li {
position: relative;
cursor: pointer;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.standard-maps li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.standard-maps a {
color: #888;
text-decoration: none;
position: relative;
display: block;
width: 250px;
}
.standard-maps a:hover {
color:#607D8B;
}
.standard-maps .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
button {
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
font-family: Arial;
}
button:hover {
background-color: #cfd8dc;
}
button.delete {
position: relative;
left: 194px;
top: -32px;
background-color: gray !important;
color: white;
}

Просмотреть файл

@ -0,0 +1,20 @@
<h2>My StandardMaps</h2>
<!--<div>
<label>StandardMap name:
<input #standardMapName />
</label>
<button (click)="add(standardMapName.value); standardMapName.value=''">
add
</button>
</div>
<ul class="standard-maps">
<li *ngFor="let standardMap of standardMaps">
<a routerLink="/detail/{{standardMap.id}}">
<span class="badge">{{standardMap.id}}</span> {{standardMap.name}}
</a>
<button class="delete" title="delete standardMap"
(click)="delete(standardMap)">x</button>
</li>
</ul>-->

Просмотреть файл

@ -0,0 +1,27 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { StandardMapsComponent } from './standard-maps.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('StandardMapsComponent', () => {
let component: StandardMapsComponent;
let fixture: ComponentFixture<StandardMapsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StandardMapsComponent ],
imports: [RouterTestingModule.withRoutes([]), HttpClientTestingModule],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StandardMapsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

Просмотреть файл

@ -0,0 +1,34 @@
import { Component, OnInit } from '@angular/core';
//import { StandardMap } from '../standard-map';
@Component({
selector: 'app-standard-maps',
templateUrl: './standard-maps.component.html',
styleUrls: ['./standard-maps.component.css']
})
export class StandardMapsComponent implements OnInit {
//standardMaps: StandardMap[];
constructor() { }
ngOnInit() {
//this.getStandardMaps();
}
//getStandardMaps(): void {
//}
//
//add(name: string): void {
// //name = name.trim();
// //if (!name) { return; }
// //this.standardMapService.addStandardMap({ name } as StandardMap)
// // .subscribe(standardMap => {
// // this.standardMaps.push(standardMap);
// // });
//}
//
//delete(standardMap: StandardMap): void {
//}
}

13207
src/assets/db.json Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>github-circle-white-transparent</title><path d="M10 0C4.477 0 0 4.477 0 10c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33.85 0 1.71.11 2.5.33 1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V19c0 .27.16.59.67.5C17.14 18.16 20 14.42 20 10A10 10 0 0 0 10 0z" fill="#FFF" fill-rule="evenodd"/></svg>

11
src/browserslist Normal file
Просмотреть файл

@ -0,0 +1,11 @@
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11

Просмотреть файл

@ -0,0 +1,3 @@
export const environment = {
production: true
};

Просмотреть файл

@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

Двоичные данные
src/favicon.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 12 KiB

16
src/index.html Normal file
Просмотреть файл

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Standard Maps Explorer</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

31
src/karma.conf.js Normal file
Просмотреть файл

@ -0,0 +1,31 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

11
src/main.ts Normal file
Просмотреть файл

@ -0,0 +1,11 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);

88
src/polyfills.ts Normal file
Просмотреть файл

@ -0,0 +1,88 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/**
* If the application will be indexed by Google Search, the following is required.
* Googlebot uses a renderer based on Chrome 41.
* https://developers.google.com/search/docs/guides/rendering
**/
// import 'core-js/es6/array';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following for the Reflect API. */
/**
* DO NOT REMOVE
* By default, Reflect polyfills are auto-included by the CLI and
* are required for JIT compilation. StackBlitz examples are
* compiled using JIT.
*/
import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
**/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
*/
// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
/*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*/
// (window as any).__Zone_enable_cross_context_check = true;
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

15
src/styles.css Normal file
Просмотреть файл

@ -0,0 +1,15 @@
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
body {
margin: 0;
}
.basic-container {
padding: 0px;
}
.mat-drawer-content {
padding: 20px;
}

20
src/test.ts Normal file
Просмотреть файл

@ -0,0 +1,20 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

21
src/tsconfig.app.json Normal file
Просмотреть файл

@ -0,0 +1,21 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts",
"**/*.avoid.ts",
"**/*.0.ts",
"**/*.1.ts",
"**/*.1b.ts",
"**/*.2.ts",
"**/*.3.ts",
"**/*.4.ts",
"**/*.5.ts",
"**/*.6.ts",
"**/*.7.ts"
]
}

18
src/tsconfig.spec.json Normal file
Просмотреть файл

@ -0,0 +1,18 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"test.ts",
"polyfills.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

17
src/tslint.json Normal file
Просмотреть файл

@ -0,0 +1,17 @@
{
"extends": "../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
]
}
}

24
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,24 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"module": "es2015",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
}
}

131
tslint.json Normal file
Просмотреть файл

@ -0,0 +1,131 @@
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
"forin": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"no-output-on-prefix": true,
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true
}
}

14
utils/ExtractHL.bas Normal file
Просмотреть файл

@ -0,0 +1,14 @@
Attribute VB_Name = "Module1"
Sub ExtractHL()
Dim HL As Hyperlink
For Each HL In ActiveSheet.Hyperlinks
Set c = HL.Parent.TopLeftCell
If Not IsNull(c) Then
Set c = ActiveSheet.Cells(c.Row, c.Column)
Debug.Print (c)
c.Value = c.Text + "[" + HL.Address + "#" + HL.SubAddress + "]"
Debug.Print (c.Text + HL.Address)
End If
Next
End Sub

293
utils/converter.js Normal file
Просмотреть файл

@ -0,0 +1,293 @@

var Excel = require('exceljs');
var inputFile = "./src/app/data/notcheckedin/MSFT ISO 27552 Country Mapping - Merged- Macro.xlsm";
var inputFileGdpr = "./src/app/data/notcheckedin/27552 to GDPR.xlsx";
var importer = require("./importer");
var keepNWords = function (input, n) {
var frags = input.split(" ");
var result = frags.slice(0, n).join(" ");
if (frags.length > n)
result += "...";
return result;
//return input.replace(new RegExp("(([^\s]+\s\s*){" + n + "})(.*)"),"$1…");
}
var removeSpecialCharacters = function (input) {
return input.replace(/[^\w\s]/gi, '');
}
var nestDoc = function (doc) {
var result = { "children" : [] };
importer.mergeDoc(doc, result);
return result;
}
var reduceDoc = function(doc) {
var result = { };
for (var i = 0; i < doc.length; ++i)
{
var newCell = doc[i];
var copywriteWordLimit = 25;
newCell.body = keepNWords(newCell.body, copywriteWordLimit);
if (newCell.id in result)
{
var lastCell = result[newCell.id];
//console.log("merge: " + newCell.id);
//console.log("with: " + lastCell.id);
// merge
if (!lastCell.body.includes(newCell.body))
{
// append body
lastCell.body += ' \n' + newCell.body;
}
importer.mergeLinks(newCell, lastCell);
}
else
{
result[newCell.id] = newCell;
}
}
var keys = Object.keys(result).sort();
var objs = keys.map(v => result[v]);
return objs;
}
var makeDoc = function (doc, type) {
doc.type = type;
doc.rev = 1;
return doc;
}
var mainMapping = function() {
// read from a file
var workbook = new Excel.Workbook();
return workbook.xlsx.readFile(inputFile)
.then(function() {
// use workbook
var sheetName = 'ISO 27552 Country Mapping';
var worksheet = workbook.getWorksheet(sheetName);
// build iso doc
var sectionColumn = worksheet.getColumn(1);
var sectionsByRow = { };
var unmappedRows = {};
sectionColumn.eachCell({ includeEmpty: true }, function(cell, rowNumber) {
// see if this is one of our unmapped/red rows
var row = worksheet.getRow(rowNumber);
row.eachCell({ includeEmpty: true }, function(cell, col) {
if (cell.fill && cell.fill.fgColor && cell.fill.fgColor.argb == 'FFF04C3E')
{
//console.log(JSON.stringify(cell.fill) + cell.text);
unmappedRows[rowNumber] = col;
}
});
if (!(rowNumber in unmappedRows))
{
var section = cell.text.match(/(\d.*)/); // must start with a number
if (section)
{
sectionsByRow[rowNumber] = importer.normalizePath(section[1])
}
}
});
var allDocs = [];
var isoDoc = [];
var descriptionColumn = worksheet.getColumn(2);
descriptionColumn.eachCell({ includeEmpty: true }, function(cell, rowNumber) {
var section = sectionsByRow[rowNumber];
if (section)
{
isoDoc.push({
id: section,
section: section,
body: cell.text
});
}
});
isoDoc = makeDoc(nestDoc(reduceDoc(isoDoc)), "ISO");
//console.log(JSON.stringify(isoDoc, null, 4));
allDocs.push(isoDoc);
var headerRow = worksheet.getRow(1);
headerRow.eachCell(function(cell, colNumber) {
if (colNumber <= 2)
return; // skip iso columns.
console.log('Header ' + colNumber + ' = ' + cell.text);
var newDoc = [];
var sectionColumn = worksheet.getColumn(colNumber);
sectionColumn.eachCell({ includeEmpty: true }, function(cell, rowNumber) {
var bodyOverride = null;
if (rowNumber in unmappedRows && unmappedRows[rowNumber] == colNumber)
{
// if it's unmapped/red, there is special handling of the text source
var row = worksheet.getRow(rowNumber);
cell = row.getCell(1); // section column
bodyOverride = row.getCell(2).text; // description column
}
var section = cell.text.match(/.*\((.*[0-9].*)\).*/);
if (section)
{
var body = cell.text;
// Look for hyperlink in brackets at end of line
var hyperlink = body.match(/\[http(.*)\]/);
if (hyperlink)
{
// format hyperlink
hyperlink = 'http' + hyperlink[1];
// trim hyperlink from the end
body = body.slice(0, -(hyperlink.length + 2)); // + 2 for brackets
}
var normalized = importer.normalizePath(section[1]);
var sectionText = normalized;
if (bodyOverride)
{
body += " - " + bodyOverride;
}
var isoSection = sectionsByRow[rowNumber];
links = isoSection
? [ {
"id": isoSection,
"type": "ISO"
}
]
: [];
newDoc.push({
id: normalized,
section: sectionText,
body: body,
hyperlink: hyperlink,
links: links
});
}
});
newDoc = makeDoc(nestDoc(reduceDoc(newDoc)), removeSpecialCharacters(cell.text));
//console.log(JSON.stringify(newDoc, null, 4));
allDocs.push(newDoc);
});
//console.log(JSON.stringify(worksheet.rowCount));
return allDocs;
});
}
var gdprMapping = function() {
// read from a file
var workbook = new Excel.Workbook();
return workbook.xlsx.readFile(inputFileGdpr)
.then(function() {
// use workbook
var sheetName = 'Sheet1';
var worksheet = workbook.getWorksheet(sheetName);
// build iso doc
var sectionColumn = worksheet.getColumn(1);
var sectionsByRow = { };
var allDocs = [];
var isoDoc = [];
var gdprDoc = [];
var gdprArticlesColIndex = 2;
var gdprTextColIndex = 3;
var titleColIndex = 4;
var descriptionColIndex = 5;
sectionColumn.eachCell({ includeEmpty: true }, function(cell, rowNumber) {
var section = cell.text.match(/(\d.*)/); // must start with a number
if (section)
{
var sectionId = importer.normalizePath(section[1]);
sectionsByRow[rowNumber] = sectionId;
var row = worksheet.getRow(rowNumber);
var gdprIds = row.getCell(gdprArticlesColIndex).text.split(",").map(v => v.match(/(\w+)*/g).filter(m => m).join("."));
var gdprText = row.getCell(gdprTextColIndex).text.split("\r\n\r\n");
var gdprZipped = gdprIds.map((v, i, a) => {
return {
id: v,
section: v,
body: gdprText[i],
links: [ {
"id": sectionId,
"type": "ISO"
}
]
}
});
gdprDoc = gdprDoc.concat(gdprZipped);
isoDoc.push({
id: sectionId,
section: sectionId + " - " + row.getCell(titleColIndex).text,
body: row.getCell(descriptionColIndex).text
});
}
});
isoDoc = makeDoc(nestDoc(reduceDoc(isoDoc)), "ISO");
gdprDoc = makeDoc(nestDoc(reduceDoc(gdprDoc)), "GDPR");
allDocs.push(isoDoc);
allDocs.push(gdprDoc);
return allDocs;
});
}
mainMapping()
.then(allDocs => {
gdprMapping()
.then(isoGdpr => {
var isoDocMain = allDocs[0];
var isoDocGdpr = isoGdpr[0];
var gdprDoc = isoGdpr[1];
allDocs.push(gdprDoc);
importer.mergeDoc(isoDocGdpr.children, isoDocMain);
//console.log(JSON.stringify(allDocs, null, 4));
importer.writeDocs(allDocs);
importer.exportXlsx(allDocs);
});
});

218
utils/importer.js Normal file
Просмотреть файл

@ -0,0 +1,218 @@

var Excel = require('exceljs');
var xlsxFile = "./src/app/data/Database.xlsx";
var outputFile = "./src/assets/db.json";
var outputFile2 = "./docs/assets/db.json"; // write one straight to the bin file so the user doesnt have to run the build pipeline.
function flatten(nodes, result) {
for (var n of nodes)
{
result.push(n);
if (n.children)
flatten(n.children, result);
}
return result;
}
function exportXlsx(allDocs) {
var workbook = new Excel.Workbook();
for (var doc of allDocs)
{
var sections = flatten(doc.children, []);
var worksheet = workbook.addWorksheet(doc.type);
worksheet.columns = [
{ key: 'id' },
{ key: 'section' },
{ key: 'body' },
{ key: 'hyperlink' },
{ key: 'isolinks' },
];
worksheet.getColumn('id').values = ["id"].concat(sections.map(v => v.id));
worksheet.getColumn('section').values = ["section"].concat(sections.map(v => v.section));
worksheet.getColumn('body').values = ["body"].concat(sections.map(v => v.body));
worksheet.getColumn('hyperlink').values = ["hyperlink"].concat(sections.map(v => v.hyperlink));
worksheet.getColumn('isolinks').values = ["isolinks"].concat(sections.map(v => v.links.map(l => l.id).join(";")));
}
workbook.xlsx.writeFile(xlsxFile)
.then(function() {
// done
});
}
function writeDocs(allDocs) {
const fs = require('fs');
let data = JSON.stringify(allDocs, null, 4);
console.log(data);
fs.writeFileSync(outputFile, data);
fs.writeFileSync(outputFile2, data);
}
var mergeLinks = function (src, dst) {
if (src.links)
{
if (!dst.links)
dst.links = [];
for (var l of src.links)
{
if (!dst.links.find(n => n.id == l.id && n.type == l.type))
{
dst.links.push(l);
}
}
}
}
var breakPath = function (path) {
if (!path)
return [];
var frags = path.split(/\W+/);
frags = frags.filter(f => f);
return frags;
}
var normalizePath = function (path) {
var frags = breakPath(path);
var assembled = frags.join(".");
return assembled;
}
var findOrCreateSection = function (root, id) {
var frags = breakPath(id);
var assembled = "";
for (var f of frags)
{
if (assembled.length)
assembled += '.';
assembled += f;
var child = root.children ? root.children.find(c => c.id == assembled) : null;
if (child)
root = child;
else
{
var node = {
"id": assembled,
"frag": f, // sortable fragment
"section": assembled,
"children": [],
"links": []
};
root.children.push(node);
root = node;
}
}
return root;
}
var recursiveSort = function (doc) {
doc.children.sort((a,b) => {
if (!isNaN(a.frag) && !isNaN(b.frag))
return parseInt(a.frag) - parseInt(b.frag);
return a.frag - b.frag;
});
for (var c of doc.children)
recursiveSort(c);
}
var mergeDocRecursive = function (src, dst) {
for (var d of src)
{
var node = findOrCreateSection(dst, d.id);
// copy properties
node.id = d.id;
node.section = d.section;
node.body = d.body;
node.hyperlink = d.hyperlink;
if (d.links)
mergeLinks(d, node);
if (d.children)
mergeDocRecursive(d.children, dst);
}
}
var mergeDoc = function (src, dst) {
mergeDocRecursive(src, dst);
recursiveSort(dst);
return dst;
}
function importXlsx() {
var workbook = new Excel.Workbook();
return workbook.xlsx.readFile(xlsxFile)
.then(function() {
var allDocs = [];
workbook.eachSheet(function(worksheet, sheetId) {
console.log(worksheet.name);
var idsCol = worksheet.getColumn(1);
var ids = [];
var doc = {
"type": worksheet.name,
"rev": 1,
"children": []
};
var newChildren = [];
idsCol.eachCell({ includeEmpty: true }, function(cell, rowNumber) {
if (rowNumber == 1)
return; // skip the header row
var row = worksheet.getRow(rowNumber);
var section = row.getCell(2).text;
var body = row.getCell(3).text;
var hyperlink = row.getCell(4).text;
var isolinks = row.getCell(5).text;
var links = isolinks.split(';').filter(v => v).map(v => { return {
"id": v,
"type": "ISO"
}; });
newChildren.push({
id: cell.text,
section: section,
body: body.length ? body : undefined,
hyperlink: hyperlink.length ? hyperlink : undefined,
links: links
});
});
mergeDoc(newChildren, doc);
allDocs.push(doc);
});
writeDocs(allDocs);
});
};
module.exports = {
exportXlsx,
writeDocs,
mergeDoc,
normalizePath,
mergeLinks,
}
importXlsx();