Initial commit for gisaf/fastapi
This commit is contained in:
commit
adce44722f
1361 changed files with 42521 additions and 0 deletions
12
.browserslistrc
Normal file
12
.browserslistrc
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
> 0.5%
|
||||||
|
last 2 versions
|
||||||
|
Firefox ESR
|
||||||
|
not dead
|
||||||
|
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
13
.editorconfig
Normal file
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
|
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.angular/cache
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage/*
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# e2e
|
||||||
|
/e2e/*.js
|
||||||
|
/e2e/*.map
|
||||||
|
|
||||||
|
#System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
31
README.md
Normal file
31
README.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# GisafApp
|
||||||
|
|
||||||
|
This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.24.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||||
|
Before running the tests make sure you are serving the app via `ng serve`.
|
||||||
|
|
||||||
|
## Deploying to Github Pages
|
||||||
|
|
||||||
|
Run `ng github-pages:deploy` to deploy to Github Pages.
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
184
angular.json
Normal file
184
angular.json
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"gisaf-app": {
|
||||||
|
"root": "",
|
||||||
|
"projectType": "application",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"tsConfig": "src/tsconfig.app.json",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "src/assets",
|
||||||
|
"output": "/assets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "favicon.ico",
|
||||||
|
"input": "src",
|
||||||
|
"output": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "src/undefined",
|
||||||
|
"output": "/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
{
|
||||||
|
"input": "node_modules/maplibre-gl/dist/maplibre-gl.css",
|
||||||
|
"inject": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/styles.css",
|
||||||
|
"inject": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/icons.css",
|
||||||
|
"inject": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/gisaf-icons.css",
|
||||||
|
"inject": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scripts": [
|
||||||
|
"node_modules/plotly.js-basic-dist-min/plotly-basic.min.js"
|
||||||
|
],
|
||||||
|
"allowedCommonJsDependencies": [
|
||||||
|
"@turf/bbox",
|
||||||
|
"@turf/meta",
|
||||||
|
"graphql-tag",
|
||||||
|
"suggestions",
|
||||||
|
"subscriptions-transport-ws",
|
||||||
|
"zen-observable",
|
||||||
|
"maplibre-gl",
|
||||||
|
"@mapbox/point-geometry"
|
||||||
|
],
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false,
|
||||||
|
"namedChunks": true
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"src": "src/environments/environment.ts",
|
||||||
|
"replaceWith": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "gisaf-app:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "gisaf-app:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "gisaf-app:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"karmaConfig": "./karma.conf.js",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "src/tsconfig.spec.json",
|
||||||
|
"scripts": [],
|
||||||
|
"styles": [
|
||||||
|
{
|
||||||
|
"input": "node_modules/maplibre-gl/dist/maplibre-gl.css",
|
||||||
|
"inject": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/styles.css",
|
||||||
|
"inject": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/icons.css",
|
||||||
|
"inject": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "src/assets",
|
||||||
|
"output": "/assets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "favicon.ico",
|
||||||
|
"input": "src",
|
||||||
|
"output": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "src/undefined",
|
||||||
|
"output": "/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gisaf-app-e2e": {
|
||||||
|
"root": "",
|
||||||
|
"projectType": "application",
|
||||||
|
"cli": {},
|
||||||
|
"schematics": {},
|
||||||
|
"architect": {
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
|
"options": {
|
||||||
|
"protractorConfig": "./protractor.conf.js",
|
||||||
|
"devServerTarget": "gisaf-app:serve"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"prefix": "app",
|
||||||
|
"style": "css"
|
||||||
|
},
|
||||||
|
"@schematics/angular:directive": {
|
||||||
|
"prefix": "app"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
|
}
|
||||||
|
}
|
33
karma.conf.js
Normal file
33
karma.conf.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// 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
|
||||||
|
},
|
||||||
|
angularCli: {
|
||||||
|
environment: 'dev'
|
||||||
|
},
|
||||||
|
reporters: ['progress', 'kjhtml'],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: ['Chrome'],
|
||||||
|
singleRun: false
|
||||||
|
});
|
||||||
|
};
|
14554
package-lock.json
generated
Normal file
14554
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
87
package.json
Normal file
87
package.json
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
{
|
||||||
|
"name": "gisaf-app",
|
||||||
|
"displayName": "Gisaf",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"description": "Gisaf Geomatics",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.bluelightav.org:2222/gisaf.git"
|
||||||
|
},
|
||||||
|
"author": "Philippe May",
|
||||||
|
"angular-cli": {},
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng serve --proxy-config proxy.conf.json --watch",
|
||||||
|
"build": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build",
|
||||||
|
"test": "ng test",
|
||||||
|
"lint": "ng lint",
|
||||||
|
"e2e": "ng e2e",
|
||||||
|
"generate-client": "openapi --input http://127.0.0.1:5000/openapi.json --output ./src/app/openapi --client angular --useOptions --useUnionTypes"
|
||||||
|
},
|
||||||
|
"licenses": [
|
||||||
|
{
|
||||||
|
"type": "MIT",
|
||||||
|
"url": "https://github.com/angular/angular.io/blob/master/LICENSE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^15.2.1",
|
||||||
|
"@angular/cdk": "^15.2.1",
|
||||||
|
"@angular/common": "^15.2.1",
|
||||||
|
"@angular/compiler": "^15.2.1",
|
||||||
|
"@angular/core": "^15.2.1",
|
||||||
|
"@angular/forms": "^15.2.1",
|
||||||
|
"@angular/material": "^15.2.1",
|
||||||
|
"@angular/platform-browser": "^15.2.1",
|
||||||
|
"@angular/platform-browser-dynamic": "^15.2.1",
|
||||||
|
"@angular/platform-server": "^15.2.1",
|
||||||
|
"@angular/router": "^15.2.1",
|
||||||
|
"@apollo/client": "^3.4.16",
|
||||||
|
"@mapbox/point-geometry": "^0.1.0",
|
||||||
|
"@maplibre/ngx-maplibre-gl": "^13.0.0",
|
||||||
|
"@turf/bbox": "^6.5.0",
|
||||||
|
"@turf/distance": "^6.5.0",
|
||||||
|
"angular-plotly.js": "^4.0.4",
|
||||||
|
"apollo-angular": "^4.2.1",
|
||||||
|
"core-js": "^2.6.3",
|
||||||
|
"graphql": "^15.6.1",
|
||||||
|
"maplibre-gl": "^2.4.0",
|
||||||
|
"ngx-flexible-layout": "15.0.1",
|
||||||
|
"plotly.js-basic-dist-min": "^2.8.1",
|
||||||
|
"rxjs": "^7.4.0",
|
||||||
|
"subscriptions-transport-ws": "^0.9.17",
|
||||||
|
"ts-helpers": "^1.1.2",
|
||||||
|
"zone.js": "~0.11.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^15.2.1",
|
||||||
|
"@angular/cli": "^15.2.1",
|
||||||
|
"@angular/compiler-cli": "^15.2.1",
|
||||||
|
"@angular/language-service": "^15.2.1",
|
||||||
|
"@types/core-js": "^2.5.0",
|
||||||
|
"@types/geojson": "^7946.0.7",
|
||||||
|
"@types/jasmine": "~3.6.0",
|
||||||
|
"@types/jasminewd2": "^2.0.2",
|
||||||
|
"@types/node": "^12.11.1",
|
||||||
|
"@types/plotly.js-dist-min": "^2.3.0",
|
||||||
|
"clean-css": "^4.2.1",
|
||||||
|
"codelyzer": "^6.0.0",
|
||||||
|
"fontnik": "^0.7.1",
|
||||||
|
"jasmine-core": "~3.6.0",
|
||||||
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
|
"karma": "~6.3.2",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-cli": "^2.0.0",
|
||||||
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
|
"karma-jasmine": "~4.0.0",
|
||||||
|
"openapi-typescript-codegen": "^0.27.0",
|
||||||
|
"protractor": "~7.0.0",
|
||||||
|
"source-map-explorer": "^2.2.2",
|
||||||
|
"ts-node": "^8.0.2",
|
||||||
|
"tslib": "^2.0.0",
|
||||||
|
"tslint": "~6.1.0",
|
||||||
|
"typescript": "~4.8.4"
|
||||||
|
}
|
||||||
|
}
|
28
protractor.conf.js
Normal file
28
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: [
|
||||||
|
'./e2e/**/*.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: 'e2e/tsconfig.e2e.json'
|
||||||
|
});
|
||||||
|
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||||
|
}
|
||||||
|
};
|
37
proxy.conf.json
Normal file
37
proxy.conf.json
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"/static": {
|
||||||
|
"target": "http://127.0.0.1:5000",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
"/gj": {
|
||||||
|
"target": "http://127.0.0.1:5000",
|
||||||
|
"secure": false,
|
||||||
|
"ws": true
|
||||||
|
},
|
||||||
|
"/sched": {
|
||||||
|
"target": "http://127.0.0.1:8000",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
"/_sched": {
|
||||||
|
"target": "http://127.0.0.1:8000",
|
||||||
|
"secure": false,
|
||||||
|
"ws": true
|
||||||
|
},
|
||||||
|
"/api": {
|
||||||
|
"target": "http://127.0.0.1:5000",
|
||||||
|
"secure": false,
|
||||||
|
"ws": true
|
||||||
|
},
|
||||||
|
"/static/tiles": {
|
||||||
|
"target": "/home/phil/gisaf_misc/map/tiles",
|
||||||
|
"secure": false,
|
||||||
|
"changeOrigin": true,
|
||||||
|
"pathRewrite": {
|
||||||
|
"^/static/tiles" : ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/terrain": {
|
||||||
|
"target": "http://127.0.0.1:8899",
|
||||||
|
"secure": false
|
||||||
|
}
|
||||||
|
}
|
7
src/app/_models/user.ts
Normal file
7
src/app/_models/user.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export class User {
|
||||||
|
constructor(
|
||||||
|
public userName: string,
|
||||||
|
public token: string,
|
||||||
|
public password?: string,
|
||||||
|
) {}
|
||||||
|
}
|
232
src/app/_services/actions.service.ts
Normal file
232
src/app/_services/actions.service.ts
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
|
||||||
|
import { Observable, pipe, BehaviorSubject } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { Apollo, gql } from 'apollo-angular'
|
||||||
|
|
||||||
|
import { Tag } from '../info/info-tags/tags.service'
|
||||||
|
import { TaggedLayer, TaggedFeature, FormFieldInput } from '../info/info-data.service'
|
||||||
|
|
||||||
|
export class ActionParam {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public type: string,
|
||||||
|
public dflt: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Action {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public roles: string[],
|
||||||
|
public params: ActionParam[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ActionsStore {
|
||||||
|
constructor(
|
||||||
|
public store: string,
|
||||||
|
public actions: Action[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ActionResult {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public message: string,
|
||||||
|
public taggedLayers: TaggedLayer[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ActionResults {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public message: string,
|
||||||
|
public actionResults: ActionResult[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ActionsResults {
|
||||||
|
constructor(
|
||||||
|
public message: string,
|
||||||
|
public actionResults: ActionResults[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ActionAction {
|
||||||
|
constructor(
|
||||||
|
public plugin: string,
|
||||||
|
public name: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Store {
|
||||||
|
constructor(
|
||||||
|
public store: string,
|
||||||
|
public actions: ActionAction[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getTagsActionsQuery = gql`
|
||||||
|
query actionsPlugins {
|
||||||
|
actionsPlugins {
|
||||||
|
store
|
||||||
|
actions {
|
||||||
|
name
|
||||||
|
roles
|
||||||
|
params {
|
||||||
|
name
|
||||||
|
type
|
||||||
|
dflt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const executeTagsActionsQuery = gql`
|
||||||
|
mutation executeAction(
|
||||||
|
$stores: [String]!,
|
||||||
|
$ids: [[String]]!,
|
||||||
|
$names: [String]!,
|
||||||
|
$params: [ActionParamInput],
|
||||||
|
$formFields: [FormFieldInput]
|
||||||
|
) {
|
||||||
|
executeAction(
|
||||||
|
stores: $stores,
|
||||||
|
ids: $ids,
|
||||||
|
names: $names,
|
||||||
|
params: $params,
|
||||||
|
formFields: $formFields,
|
||||||
|
) {
|
||||||
|
result {
|
||||||
|
message
|
||||||
|
actionResults {
|
||||||
|
name
|
||||||
|
message
|
||||||
|
actionResults {
|
||||||
|
message
|
||||||
|
taggedLayers {
|
||||||
|
store
|
||||||
|
taggedFeatures {
|
||||||
|
id
|
||||||
|
lon
|
||||||
|
lat
|
||||||
|
tags {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ActionsService {
|
||||||
|
actionsStores: ActionsStore[] = []
|
||||||
|
|
||||||
|
public actionsProviderService = new BehaviorSubject<ActionsStore[]>([])
|
||||||
|
public actionsProviderService$ = this.actionsProviderService.asObservable()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private apollo: Apollo,
|
||||||
|
) {
|
||||||
|
this.getTagsActionsStores().subscribe(
|
||||||
|
actionsStores => {
|
||||||
|
this.actionsStores = actionsStores
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTagsActionsStores(): Observable<ActionsStore[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getTagsActionsQuery,
|
||||||
|
}).pipe(map(
|
||||||
|
res => {
|
||||||
|
let actionStores = res['data']['actionsPlugins'].map(pipe(
|
||||||
|
store => new Store(
|
||||||
|
store['store'],
|
||||||
|
store['actions'].map(
|
||||||
|
action => new Action(
|
||||||
|
action['name'],
|
||||||
|
action['roles'],
|
||||||
|
action['params'].map(
|
||||||
|
param => new ActionParam(
|
||||||
|
param['name'],
|
||||||
|
param['type'],
|
||||||
|
param['dflt']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
this.actionsProviderService.next(actionStores)
|
||||||
|
return actionStores
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
public execute(
|
||||||
|
stores: string[],
|
||||||
|
ids: string[][],
|
||||||
|
actionNames: string[],
|
||||||
|
params: ActionParam[],
|
||||||
|
formFields?: FormFieldInput[]
|
||||||
|
): Observable<ActionsResults> {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: executeTagsActionsQuery,
|
||||||
|
variables: {
|
||||||
|
stores: stores,
|
||||||
|
ids: ids,
|
||||||
|
names: actionNames,
|
||||||
|
params: params,
|
||||||
|
formFields: formFields
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
result => result['data']['executeAction']['result'].map(
|
||||||
|
res => new ActionsResults(
|
||||||
|
res['message'],
|
||||||
|
res['actionResults'].map(
|
||||||
|
r => new ActionResults(
|
||||||
|
r['name'],
|
||||||
|
r['message'],
|
||||||
|
r['actionResults'] && r['actionResults'].map(
|
||||||
|
actionResults => new ActionResult(
|
||||||
|
actionResults['name'],
|
||||||
|
actionResults['message'],
|
||||||
|
(actionResults['taggedLayers'] || []).map(
|
||||||
|
taggedLayer => new TaggedLayer(
|
||||||
|
taggedLayer['store'],
|
||||||
|
taggedLayer['taggedFeatures'].map(
|
||||||
|
taggedFeature => new TaggedFeature(
|
||||||
|
taggedFeature['id'],
|
||||||
|
taggedFeature['lon'],
|
||||||
|
taggedFeature['lat'],
|
||||||
|
taggedFeature['tags'].map(
|
||||||
|
tag => new Tag(
|
||||||
|
tag['key'],
|
||||||
|
tag['value']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
404
src/app/_services/apollo.service.ts
Normal file
404
src/app/_services/apollo.service.ts
Normal file
|
@ -0,0 +1,404 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Validators, UntypedFormGroup, UntypedFormControl } from '@angular/forms'
|
||||||
|
|
||||||
|
import { MatTableDataSource } from '@angular/material/table'
|
||||||
|
|
||||||
|
import { Observable, forkJoin } from 'rxjs'
|
||||||
|
import { map, mergeMap } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { Apollo, gql } from 'apollo-angular'
|
||||||
|
|
||||||
|
const fieldTypeMap = {
|
||||||
|
Int: 'number',
|
||||||
|
Float: 'number',
|
||||||
|
Boolean: 'checkbox',
|
||||||
|
}
|
||||||
|
|
||||||
|
const introspectionQuery = gql`
|
||||||
|
query introspect ($modelName: String!) {
|
||||||
|
__type(name: $modelName) {
|
||||||
|
name
|
||||||
|
fields {
|
||||||
|
name
|
||||||
|
type {
|
||||||
|
name
|
||||||
|
kind
|
||||||
|
ofType {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const inspectionQuery = gql`
|
||||||
|
query inspect ($modelName: String!) {
|
||||||
|
inspect(modelName: $modelName) {
|
||||||
|
pkFields
|
||||||
|
relation_fields
|
||||||
|
joins {
|
||||||
|
name
|
||||||
|
target
|
||||||
|
rel_field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const dashboardPageQuery = gql`
|
||||||
|
query dashboard_page ($group: String!, $name: String!) {
|
||||||
|
dashboard_page(group: $group, name: $name) {
|
||||||
|
name
|
||||||
|
group
|
||||||
|
description
|
||||||
|
html
|
||||||
|
notebook
|
||||||
|
time
|
||||||
|
dfData
|
||||||
|
plotData
|
||||||
|
attachment
|
||||||
|
expandedPanes
|
||||||
|
sections {
|
||||||
|
name
|
||||||
|
plot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
export class FieldIntrospection {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public type: string,
|
||||||
|
public primary: boolean = false,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get inputType() {
|
||||||
|
return fieldTypeMap[this.type] || 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
get isTextArea() {
|
||||||
|
return this.type == 'JSONString'
|
||||||
|
}
|
||||||
|
|
||||||
|
get isText() {
|
||||||
|
return this.type == 'String' || this.type == 'Int' || this.type == 'Float'
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCheckbox() {
|
||||||
|
return this.type == 'Boolean'
|
||||||
|
}
|
||||||
|
|
||||||
|
get validator() {
|
||||||
|
if (this.type == 'ID') {
|
||||||
|
return Validators.required
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ModelIntrospection {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public fields: FieldIntrospection[],
|
||||||
|
public relations: object[],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
pkFields(): FieldIntrospection[] {
|
||||||
|
return this.fields.filter(f => f.primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
columns(): string[] {
|
||||||
|
return this.fields.map(f => f.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
columnsForGql(): string {
|
||||||
|
return this.columns().join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
get mutationFields(): string {
|
||||||
|
return this.fields.map(t => '$' + t.name + ':' + t.type).join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
get mutationVars(): string {
|
||||||
|
return this.fields.map(t => t.name + ':$' + t.name).join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
get mutationData(): string {
|
||||||
|
return this.columns().join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
getMutationQuery() {
|
||||||
|
return gql`
|
||||||
|
mutation mutation(${this.mutationFields}) {
|
||||||
|
mutation(${this.mutationVars}) {
|
||||||
|
${this.mutationData}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class JoinField {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public target: string,
|
||||||
|
public rel_field: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ModelInspection {
|
||||||
|
constructor(
|
||||||
|
public pkFields: string[],
|
||||||
|
public relations: string[],
|
||||||
|
public joins: JoinField[],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get joinedFieldNames() {
|
||||||
|
return this.joins.map(join => join.rel_field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DashboardPageSection {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public plot: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DashboardPage {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public group: string,
|
||||||
|
public errors: string = undefined,
|
||||||
|
public description: string = undefined,
|
||||||
|
public html: string = undefined,
|
||||||
|
public notebook: string = undefined,
|
||||||
|
public dfData: MatTableDataSource<object> = undefined,
|
||||||
|
public plotData: Object = undefined,
|
||||||
|
public time: Date = undefined,
|
||||||
|
public attachment: string = undefined,
|
||||||
|
public expandedPanes: string[] = [],
|
||||||
|
public sections: DashboardPageSection[] = []
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Model {
|
||||||
|
constructor(
|
||||||
|
public inspection: ModelInspection,
|
||||||
|
public introspection: ModelIntrospection,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getFormFields(formGroup: UntypedFormGroup, item): FieldIntrospection[] {
|
||||||
|
// Return the form fields and build the FormGroup controls accordingly
|
||||||
|
// XXX: move to another class, aka admin.models.Model?
|
||||||
|
let formFields = []
|
||||||
|
this.introspection.fields.forEach(
|
||||||
|
field => {
|
||||||
|
// Don't add fields which are in inspection.relations
|
||||||
|
if (this.inspection.joinedFieldNames.indexOf(field.name) >= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let control = new UntypedFormControl(field.name, field.validator)
|
||||||
|
control.setValue(item[field.name])
|
||||||
|
formGroup.addControl(field.name, control)
|
||||||
|
formFields.push(field)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.inspection.joins.forEach(
|
||||||
|
join => {
|
||||||
|
let control = new UntypedFormControl()
|
||||||
|
control.setValue(item[join.rel_field])
|
||||||
|
formGroup.addControl(join.name, control)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return formFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ModelDataService {
|
||||||
|
constructor(
|
||||||
|
private apollo: Apollo
|
||||||
|
) {}
|
||||||
|
|
||||||
|
fullInspect(modelName): Observable<Model> {
|
||||||
|
return forkJoin([
|
||||||
|
this.inspect(modelName),
|
||||||
|
this.introspect(modelName),
|
||||||
|
]).pipe(map(
|
||||||
|
res => new Model(res[0], res[1])
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
inspect(modelName): Observable<ModelInspection> {
|
||||||
|
// Inspection (Gisaf)
|
||||||
|
return this.get(inspectionQuery, {'modelName': modelName}).pipe(map(
|
||||||
|
res => {
|
||||||
|
let joins = res['inspect']['joins'].map(
|
||||||
|
join => new JoinField(join['name'], join['target'], join['rel_field'])
|
||||||
|
)
|
||||||
|
return new ModelInspection(
|
||||||
|
res['inspect']['pkFields'],
|
||||||
|
res['inspect']['relation_fields'],
|
||||||
|
joins
|
||||||
|
)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
introspect(modelName): Observable<ModelIntrospection> {
|
||||||
|
// Introspection (Graphql)
|
||||||
|
return this.get(introspectionQuery, {'modelName': modelName}).pipe(map(
|
||||||
|
res => {
|
||||||
|
var relations = []
|
||||||
|
let _fields = res['__type']['fields']
|
||||||
|
// Find the primary keys
|
||||||
|
var fields = _fields.filter(f => !f.type.name && f.type.kind=='NON_NULL').map(
|
||||||
|
f => new FieldIntrospection(f.name, f['type']['ofType']['name'], true)
|
||||||
|
)
|
||||||
|
var pkFieldNames = fields.map(f => f.name)
|
||||||
|
_fields.forEach(
|
||||||
|
resField => {
|
||||||
|
let name = resField['name']
|
||||||
|
let type = resField['type']
|
||||||
|
// Skip primary keys
|
||||||
|
if (pkFieldNames.indexOf(name) != -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (name == 'geom') {
|
||||||
|
// Remove geom column
|
||||||
|
// XXX: Should be marked as geojson type
|
||||||
|
// and might be a link to the map or something like that
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type['kind'] == 'OBJECT') {
|
||||||
|
// Relation
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fields.push(new FieldIntrospection(name, type['name']))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return new ModelIntrospection(modelName, fields, relations)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
mutate(model: ModelIntrospection, values: object): Observable<object> {
|
||||||
|
let mutationQuery = model.getMutationQuery()
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: mutationQuery,
|
||||||
|
variables: values,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveItemData(modelName: string, pk: string): Observable<object> {
|
||||||
|
return this.fullInspect(modelName).pipe(
|
||||||
|
mergeMap(
|
||||||
|
model => {
|
||||||
|
// XXX: get pk type and name from model introspection
|
||||||
|
let pkField = model.introspection.pkFields()[0]
|
||||||
|
let query = gql`
|
||||||
|
query item ($pk: ID) {
|
||||||
|
${modelName} (pk:$pk) {
|
||||||
|
${model.introspection.columnsForGql()}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
return this.get(query, {pk: pk}).pipe(map(
|
||||||
|
item => {
|
||||||
|
if (item) {
|
||||||
|
return {
|
||||||
|
'item': item,
|
||||||
|
'model': model,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
all(model: ModelIntrospection, fields?: string[]): Observable<object> {
|
||||||
|
if (!fields) {
|
||||||
|
fields = model.columns()
|
||||||
|
}
|
||||||
|
const fieldList = fields.join(' ')
|
||||||
|
const query = gql`query model {${model.name}{${fieldList}}}`
|
||||||
|
return this.get(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
get(query, vars: object = {}): Observable<any> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: query,
|
||||||
|
variables: vars,
|
||||||
|
errorPolicy: 'all',
|
||||||
|
}).pipe(map(
|
||||||
|
result => {
|
||||||
|
if (result.errors) {
|
||||||
|
throw result.errors.map(err => err.message).join(', ')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DashboardDataService {
|
||||||
|
constructor(
|
||||||
|
private apollo: Apollo
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getDashboardPage(group: string, name: string): Observable<DashboardPage> {
|
||||||
|
return this.get(dashboardPageQuery, {'name': name, 'group': group}).pipe(map(
|
||||||
|
res => {
|
||||||
|
if (res['errors'] && res['errors'].length > 0) {
|
||||||
|
return new DashboardPage(
|
||||||
|
name,
|
||||||
|
group,
|
||||||
|
res['errors'].map(e => e.message).join(', '),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let page = res['dashboard_page']
|
||||||
|
return new DashboardPage(
|
||||||
|
page['name'],
|
||||||
|
page['group'],
|
||||||
|
'',
|
||||||
|
page['description'],
|
||||||
|
page['html'],
|
||||||
|
page['notebook'],
|
||||||
|
JSON.parse(page['dfData']),
|
||||||
|
JSON.parse(page['plotData']),
|
||||||
|
page['time'],
|
||||||
|
page['attachment'],
|
||||||
|
page['expandedPanes'],
|
||||||
|
page['sections'] ? page['sections'].map(
|
||||||
|
section => new DashboardPageSection(
|
||||||
|
section['name'],
|
||||||
|
section['plot']
|
||||||
|
)
|
||||||
|
) : []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
get(query, vars: object = {}): Observable<any> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: query,
|
||||||
|
variables: vars
|
||||||
|
}).pipe(map(
|
||||||
|
result => {
|
||||||
|
if (result.errors && result.errors.length > 0) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
16
src/app/_services/authentication.service.spec.ts
Normal file
16
src/app/_services/authentication.service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/* tslint:disable:no-unused-variable */
|
||||||
|
|
||||||
|
import { TestBed, inject, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { AuthenticationService } from './authentication.service';
|
||||||
|
|
||||||
|
describe('AuthenticationService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [AuthenticationService]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ...', inject([AuthenticationService], (service: AuthenticationService) => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
}));
|
||||||
|
});
|
125
src/app/_services/authentication.service.ts
Normal file
125
src/app/_services/authentication.service.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http'
|
||||||
|
import { Observable, BehaviorSubject, from, throwError } from 'rxjs'
|
||||||
|
import { map, catchError } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { User } from '../_models/user'
|
||||||
|
import { RoleReadNoUsers } from '../openapi'
|
||||||
|
|
||||||
|
interface AuthResponse {
|
||||||
|
access_token: string,
|
||||||
|
roles: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthenticationService {
|
||||||
|
user = new BehaviorSubject<User>(undefined)
|
||||||
|
user$ = this.user.asObservable()
|
||||||
|
roles: RoleReadNoUsers[] = []
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _http: HttpClient,
|
||||||
|
) {
|
||||||
|
// set token if saved in local storage
|
||||||
|
this.user.next(<User>JSON.parse(localStorage.getItem('user')))
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoggedIn() : Observable<boolean> {
|
||||||
|
if (!this.user.value) {
|
||||||
|
return from([false])
|
||||||
|
}
|
||||||
|
let body = JSON.stringify({
|
||||||
|
token: this.user.value.token,
|
||||||
|
})
|
||||||
|
return this._http.post(
|
||||||
|
'/auth/isLoggedIn',
|
||||||
|
body,
|
||||||
|
{
|
||||||
|
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
|
||||||
|
}
|
||||||
|
).pipe(
|
||||||
|
map(resp => true),
|
||||||
|
catchError(
|
||||||
|
err => {
|
||||||
|
const userName = this.user.value['userName']
|
||||||
|
this.user.next(undefined)
|
||||||
|
this.roles = []
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
return throwError(
|
||||||
|
() => new Error('Session of user "' + userName + '" expired.')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
login(userName: string, password: string): Observable<boolean> {
|
||||||
|
let body = JSON.stringify({
|
||||||
|
userName: userName,
|
||||||
|
password: password
|
||||||
|
})
|
||||||
|
return this._http.post<AuthResponse>(
|
||||||
|
'/auth/login',
|
||||||
|
body,
|
||||||
|
{
|
||||||
|
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
|
||||||
|
}
|
||||||
|
).pipe(map(
|
||||||
|
(response: AuthResponse) => {
|
||||||
|
// login successful if there's a jwt token in the response
|
||||||
|
let token = response.access_token
|
||||||
|
if (token) {
|
||||||
|
//const decodedToken = this.helper.decodeToken(token)
|
||||||
|
// store userName and jwt token in local storage to keep user logged in between page refreshes
|
||||||
|
localStorage.setItem('user',
|
||||||
|
JSON.stringify({
|
||||||
|
userName: userName,
|
||||||
|
token: token,
|
||||||
|
roles: response.roles,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('TODO: AuthenticationService roles to be set by refreshing bootstrap')
|
||||||
|
// this.roles = response.roles
|
||||||
|
|
||||||
|
// Notify
|
||||||
|
this.user.next(new User(userName, token))
|
||||||
|
|
||||||
|
// return true to indicate successful login
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
this.user.next(undefined)
|
||||||
|
this.roles = []
|
||||||
|
// return false to indicate failed login
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): boolean {
|
||||||
|
// XXX: not completly safe: the server might be down:
|
||||||
|
// We should actually *check* that the logout response is OK and display message
|
||||||
|
// clear token remove user from local storage to log user out
|
||||||
|
let has_token: boolean = this.user.value && !!this.user.value.token
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
this.user.next(undefined)
|
||||||
|
this.roles = []
|
||||||
|
|
||||||
|
// Tell server that the user has logged out
|
||||||
|
if (has_token) {
|
||||||
|
this._http.get('/auth/logout').subscribe(response => {})
|
||||||
|
}
|
||||||
|
return has_token
|
||||||
|
}
|
||||||
|
|
||||||
|
logoutAdmin(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthorized(roles: string[]) {
|
||||||
|
// Return true if at least one role in given list matches one role of the authenticated user
|
||||||
|
if (roles.length == 0) return true
|
||||||
|
return this.roles.filter(value => -1 !== roles.indexOf(value.name)).length > 0
|
||||||
|
}
|
||||||
|
}
|
16
src/app/_services/bootstrap.service.spec.ts
Normal file
16
src/app/_services/bootstrap.service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/* tslint:disable:no-unused-variable */
|
||||||
|
|
||||||
|
import { TestBed, inject, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { BootstrapService } from './bootstrap.service';
|
||||||
|
|
||||||
|
describe('BootstrapService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [BootstrapService]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ...', inject([BootstrapService], (service: BootstrapService) => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
}));
|
||||||
|
});
|
15
src/app/_services/bootstrap.service.ts
Normal file
15
src/app/_services/bootstrap.service.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
|
import { ApiService, BootstrapData } from '../openapi'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BootstrapService {
|
||||||
|
constructor(
|
||||||
|
private api: ApiService,
|
||||||
|
){ }
|
||||||
|
|
||||||
|
get(): Observable<BootstrapData> {
|
||||||
|
return this.api.bootstrapApiBootstrapGet()
|
||||||
|
}
|
||||||
|
}
|
72
src/app/_services/data.service.ts
Normal file
72
src/app/_services/data.service.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
export class MeasuresItem {
|
||||||
|
constructor(
|
||||||
|
public $uri: string,
|
||||||
|
public caption: string,
|
||||||
|
) {}
|
||||||
|
get id(): string {
|
||||||
|
return this.$uri.substr(this.$uri.lastIndexOf('/') + 1)
|
||||||
|
}
|
||||||
|
get name(): string {
|
||||||
|
return this.caption || this.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MeasuresItemsList {
|
||||||
|
constructor(
|
||||||
|
public items: MeasuresItem[]
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DataService {
|
||||||
|
constructor(private _http: HttpClient){ }
|
||||||
|
|
||||||
|
getResources(): Observable<Object[]> {
|
||||||
|
return this._http.get<Object[]>('/api/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
getList(store: string): Observable<MeasuresItem[]> {
|
||||||
|
return this._http.get<MeasuresItem[]>('/api/' + store).pipe(
|
||||||
|
map(res => res.map(
|
||||||
|
item => new MeasuresItem(item['$uri'], item['caption'])
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getValues(
|
||||||
|
store: string,
|
||||||
|
id: number,
|
||||||
|
value: string,
|
||||||
|
sampling?: string,
|
||||||
|
format: string = 'json',
|
||||||
|
rangeFrom?: string,
|
||||||
|
rangeTo?: string
|
||||||
|
): Observable<HttpResponse<Object>> {
|
||||||
|
let p = {}
|
||||||
|
let s = {}
|
||||||
|
p[store] = id
|
||||||
|
if (rangeFrom && rangeTo) {
|
||||||
|
p['time'] = {"$between": [rangeFrom, rangeTo]}
|
||||||
|
}
|
||||||
|
s['time'] = false
|
||||||
|
let params = new HttpParams()
|
||||||
|
.set('where', JSON.stringify(p))
|
||||||
|
.set('sort', JSON.stringify(s))
|
||||||
|
.set('resample', sampling)
|
||||||
|
.set('format', format)
|
||||||
|
// FIXME: add the name of the value to fetch
|
||||||
|
return this._http.get<Object>(
|
||||||
|
'/api/' + store + '/values/' + value,
|
||||||
|
{
|
||||||
|
params: params,
|
||||||
|
observe: 'response'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
102
src/app/_services/geojson.service.ts
Normal file
102
src/app/_services/geojson.service.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'
|
||||||
|
|
||||||
|
import { Observable, forkJoin } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { Apollo, gql } from 'apollo-angular'
|
||||||
|
|
||||||
|
import { WebsocketService } from '../_services/websocket.service'
|
||||||
|
|
||||||
|
const getLayerQuery = gql`
|
||||||
|
query mapboxStyle($store: String!) {
|
||||||
|
mapboxStyle (store: $store) {
|
||||||
|
paint
|
||||||
|
layout
|
||||||
|
attribution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export class MapboxStyle {
|
||||||
|
constructor(
|
||||||
|
public paint: string,
|
||||||
|
public layout: string,
|
||||||
|
public attribution: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class MapboxDataAndStyle {
|
||||||
|
constructor(
|
||||||
|
public data: object,
|
||||||
|
public style: MapboxStyle,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GeoJsonService {
|
||||||
|
constructor(
|
||||||
|
private _http: HttpClient,
|
||||||
|
private apollo: Apollo
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getLayer(url: string, params?: object): Observable<object> {
|
||||||
|
if (!params) {
|
||||||
|
params = {}
|
||||||
|
}
|
||||||
|
return this._http.get<object>(url, {
|
||||||
|
headers: <HttpHeaders>params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyle(store: string): Observable<MapboxStyle> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getLayerQuery,
|
||||||
|
variables: {
|
||||||
|
store: store
|
||||||
|
},
|
||||||
|
errorPolicy: 'all',
|
||||||
|
}).pipe(map(
|
||||||
|
result => new MapboxStyle(
|
||||||
|
result['data']['mapboxStyle']['paint'],
|
||||||
|
result['data']['mapboxStyle']['layout'],
|
||||||
|
result['data']['mapboxStyle']['attribution'],
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(url: string, store: string, params?: object): Observable<MapboxDataAndStyle> {
|
||||||
|
return forkJoin([
|
||||||
|
this.getLayer(url, params),
|
||||||
|
this.getStyle(store),
|
||||||
|
]).pipe(map(
|
||||||
|
res => new MapboxDataAndStyle(res[0], res[1])
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LiveGeoJsonService {
|
||||||
|
messages = {}
|
||||||
|
ws: any
|
||||||
|
constructor(
|
||||||
|
private wsService: WebsocketService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
connect(channel: string) {
|
||||||
|
const hostname = window.location.hostname
|
||||||
|
const port = window.location.port
|
||||||
|
let protocol = window.location.protocol == 'https:' ? 'wss:' : 'ws:'
|
||||||
|
this.wsService.connect(
|
||||||
|
protocol + '//' + hostname + ':' + port + '/gj/live/' + channel
|
||||||
|
)
|
||||||
|
return this.wsService.ws
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.wsService.disconnect()
|
||||||
|
}
|
||||||
|
}
|
30
src/app/_services/websocket.service.ts
Normal file
30
src/app/_services/websocket.service.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Subject, Observable, Observer } from 'rxjs'
|
||||||
|
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WebsocketService {
|
||||||
|
ws: WebSocketSubject<any>
|
||||||
|
|
||||||
|
public connect(url: string, openObserver?: Subject<any>, closeObserver?: Subject<any>) {
|
||||||
|
this.ws = webSocket({
|
||||||
|
url: url,
|
||||||
|
openObserver: openObserver,
|
||||||
|
closeObserver: closeObserver
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnect() {
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe(message: string) {
|
||||||
|
return this.ws.next('subscribe/' + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribe(message: string) {
|
||||||
|
return this.ws.next('unsubscribe/' + message)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
.item {
|
||||||
|
border: 1px solid grey;
|
||||||
|
margin: 0 3px;
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
<div class='item'>
|
||||||
|
<div class='path'>
|
||||||
|
{{ file.path }}
|
||||||
|
</div>
|
||||||
|
<div class='url'>
|
||||||
|
{{ file.url }}
|
||||||
|
</div>
|
||||||
|
<div class='status'>
|
||||||
|
{{ file.status }}
|
||||||
|
</div>
|
||||||
|
<div class='store'>
|
||||||
|
{{ file.store }}
|
||||||
|
</div>
|
||||||
|
<div class='time'>
|
||||||
|
{{ file.time }}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Component, OnInit, Input,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
|
||||||
|
import { ActivatedRoute } from '@angular/router'
|
||||||
|
|
||||||
|
import { AdminDataService } from '../../admin-data.service'
|
||||||
|
import { AdminBasketFile } from '../data.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-basket-item',
|
||||||
|
templateUrl: './basket-item.component.html',
|
||||||
|
styleUrls: ['./basket-item.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AdminBasketItemComponent implements OnInit {
|
||||||
|
constructor(
|
||||||
|
public adminDataService: AdminDataService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Input() file: AdminBasketFile
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
}
|
19
src/app/admin/admin-basket/admin-basket-resolver.service.ts
Normal file
19
src/app/admin/admin-basket/admin-basket-resolver.service.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'
|
||||||
|
import { Observable, forkJoin } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { AdminBasket, AdminBasketDataService } from './data.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BasketResolver implements Resolve<object> {
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private basketDataService: AdminBasketDataService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<AdminBasket> {
|
||||||
|
var name = route.paramMap.get('name')
|
||||||
|
return this.basketDataService.getBasket(name)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container mat-form-field {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container form {
|
||||||
|
border: 1px solid grey;
|
||||||
|
padding: 2px 1em;
|
||||||
|
border-radius: 0.6em;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
background-color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.importButtonGroup {
|
||||||
|
flex: 5 1 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field.store {
|
||||||
|
width: 18em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field.status {
|
||||||
|
width: 4em;
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
<div class='container'>
|
||||||
|
<form [formGroup]="formGroup">
|
||||||
|
<mat-form-field [hidden]="is_upload_field_hidden('project')">
|
||||||
|
<mat-label>Project</mat-label>
|
||||||
|
<mat-select formControlName="project">
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let item of basket.projects || adminDataService.surveyMeta.projects"
|
||||||
|
[value]="item.name">
|
||||||
|
{{ item.name }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field [hidden]="is_upload_field_hidden('surveyor')">
|
||||||
|
<mat-label>Surveyor</mat-label>
|
||||||
|
<mat-select formControlName="surveyor">
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let item of adminDataService.surveyMeta.surveyors"
|
||||||
|
[value]="item.name">
|
||||||
|
{{ item.name }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field [hidden]="is_upload_field_hidden('equipment')">
|
||||||
|
<mat-label>Equipment</mat-label>
|
||||||
|
<mat-select formControlName="equipment">
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let item of adminDataService.surveyMeta.equipments"
|
||||||
|
[value]="item.name">
|
||||||
|
{{ item.name }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class='store' [hidden]="is_upload_field_hidden('store_line_work')">
|
||||||
|
<mat-label>Store</mat-label>
|
||||||
|
<mat-select formControlName="store_line_work">
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let item of adminDataService.surveyMeta.stores_line_work"
|
||||||
|
[value]="item.name">
|
||||||
|
{{ item.name }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class='store' [hidden]="is_upload_field_hidden('store_misc')">
|
||||||
|
<mat-label>Store</mat-label>
|
||||||
|
<mat-select formControlName="store_misc">
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let item of adminDataService.surveyMeta.stores_misc"
|
||||||
|
[value]="item.name">
|
||||||
|
{{ item.name }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class='status' [hidden]="is_upload_field_hidden('status')">
|
||||||
|
<mat-label>Status</mat-label>
|
||||||
|
<mat-select formControlName="status">
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let item of adminDataService.surveyMeta.statuses"
|
||||||
|
[value]="item">
|
||||||
|
{{ item }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div class='importButtonGroup'>
|
||||||
|
<button mat-button (click)="fileInput.click()" [disabled]="!formGroup.valid">
|
||||||
|
<mat-icon>file_upload</mat-icon>
|
||||||
|
Add file
|
||||||
|
</button>
|
||||||
|
<mat-checkbox formControlName='autoImport'
|
||||||
|
matTooltip='Automatically import the content of the file in the database when its added to the basket'>
|
||||||
|
Auto import
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<input hidden (change)="onFileUpload()" #fileInput type="file" id="file">
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { Component, OnInit, Input, ViewChild, ElementRef,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy, SimpleChanges, OnChanges } from '@angular/core'
|
||||||
|
import { HttpClient } from '@angular/common/http'
|
||||||
|
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'
|
||||||
|
|
||||||
|
import { MatTableDataSource } from '@angular/material/table'
|
||||||
|
|
||||||
|
import { AdminDataService } from '../../admin-data.service'
|
||||||
|
import { AdminBasketFile, AdminBasket } from '../data.service'
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar'
|
||||||
|
import { HtmlSnackbarComponent } from '../../../custom-snackbar/custom-snackbar.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-basket-upload',
|
||||||
|
templateUrl: './basket-upload.component.html',
|
||||||
|
styleUrls: ['./basket-upload.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AdminBasketUploadComponent implements OnInit, OnChanges {
|
||||||
|
constructor(
|
||||||
|
public adminDataService: AdminDataService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private http: HttpClient,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@ViewChild('fileInput') fileInput: ElementRef
|
||||||
|
@Input() basket: AdminBasket
|
||||||
|
@Input() dataSource: MatTableDataSource<object>
|
||||||
|
|
||||||
|
//upload_fields = ['store', 'status', 'project', 'surveyor', 'equipment']
|
||||||
|
|
||||||
|
formGroup: UntypedFormGroup = new UntypedFormGroup({})
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
let defaults = this.adminDataService.surveyMeta.defaults
|
||||||
|
this.formGroup = new UntypedFormGroup({
|
||||||
|
'store_misc': new UntypedFormControl(defaults['store_misc'], [Validators.required]),
|
||||||
|
'store_line_work': new UntypedFormControl(defaults['store_line_work'], [Validators.required]),
|
||||||
|
'status': new UntypedFormControl(defaults['status'], [Validators.required]),
|
||||||
|
'project': new UntypedFormControl(defaults['project'], [Validators.required]),
|
||||||
|
'surveyor': new UntypedFormControl(defaults['surveyor'], [Validators.required]),
|
||||||
|
'equipment': new UntypedFormControl(defaults['equipment'], [Validators.required]),
|
||||||
|
'autoImport': new UntypedFormControl(true),
|
||||||
|
})
|
||||||
|
this.setupRequired()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
|
this.setupRequired()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupRequired() {
|
||||||
|
for (let field in this.formGroup.controls) {
|
||||||
|
if (this.basket.uploadFields.includes(field)) {
|
||||||
|
this.formGroup.controls[field].setValidators(Validators.required)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.formGroup.controls[field].clearValidators()
|
||||||
|
}
|
||||||
|
this.formGroup.controls[field].updateValueAndValidity({onlySelf: true})
|
||||||
|
}
|
||||||
|
this.formGroup.updateValueAndValidity({onlySelf: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileUpload() {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', <File>this.fileInput.nativeElement.files[0])
|
||||||
|
let fg = this.formGroup.getRawValue()
|
||||||
|
for (let field in fg) {
|
||||||
|
if (this.basket.uploadFields.indexOf(field) != -1) {
|
||||||
|
formData.append(field, fg[field])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.append('autoImport', this.formGroup.get('autoImport').value)
|
||||||
|
this.http.post('upload/basket/' + this.basket.name, formData).subscribe(
|
||||||
|
resp => {
|
||||||
|
let importResult = resp['import_result']
|
||||||
|
const importTime = resp['time'] || (importResult && importResult['time'])
|
||||||
|
const fileImport = new AdminBasketFile(
|
||||||
|
resp['id'],
|
||||||
|
resp['dir'],
|
||||||
|
resp['name'],
|
||||||
|
resp['url'],
|
||||||
|
resp['md5'],
|
||||||
|
importTime && new Date(importTime),
|
||||||
|
resp['comment'],
|
||||||
|
resp['status'],
|
||||||
|
resp['store'],
|
||||||
|
resp['project'],
|
||||||
|
resp['surveyor'],
|
||||||
|
resp['equipment'],
|
||||||
|
)
|
||||||
|
this.dataSource.data.push(fileImport)
|
||||||
|
this.dataSource.data = this.dataSource.data
|
||||||
|
let msg = 'File ' + fileImport.name + ' added to basket'
|
||||||
|
if (importResult) {
|
||||||
|
this.snackBar.openFromComponent(HtmlSnackbarComponent, {
|
||||||
|
data: importResult
|
||||||
|
//duration: 3000
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.snackBar.open(msg, 'Close')
|
||||||
|
}
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is_upload_field_hidden(fname: string) {
|
||||||
|
return this.basket.uploadFields.indexOf(fname)==-1
|
||||||
|
}
|
||||||
|
}
|
66
src/app/admin/admin-basket/basket.component.css
Normal file
66
src/app/admin/admin-basket/basket.component.css
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
gisaf-admin-basket-upload {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter button {
|
||||||
|
min-width: 1em!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-column-delete, .cdk-column-import {
|
||||||
|
max-width: 4em;
|
||||||
|
width: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-column-name>span {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.even {
|
||||||
|
background-color: #f5f5f512;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools .filter {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools .upload {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep th.mat-mdc-header-cell, :host ::ng-deep td.mat-mdc-cell, :host ::ng-deep td.mat-mdc-footer-cell {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep td.mat-mdc-cell {
|
||||||
|
border-bottom-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-column-delete, .mat-column-import {
|
||||||
|
width: 3em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.mat-mdc-header-cell:first-of-type, td.mat-mdc-cell:first-of-type, td.mat-mdc-footer-cell:first-of-type {
|
||||||
|
padding-left: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.mat-mdc-header-cell:last-of-type, td.mat-mdc-cell:last-of-type, td.mat-mdc-footer-cell:last-of-type {
|
||||||
|
padding-right: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
td .mat-mdc-button {
|
||||||
|
min-width: inherit ! important;
|
||||||
|
}
|
107
src/app/admin/admin-basket/basket.component.html
Normal file
107
src/app/admin/admin-basket/basket.component.html
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<div fxFlexFill fxLayout='column'>
|
||||||
|
<h1>Basket: {{ basket.name }}</h1>
|
||||||
|
<div class='tools'>
|
||||||
|
<mat-form-field class='filter'>
|
||||||
|
<mat-label>Filter table</mat-label>
|
||||||
|
<input matInput
|
||||||
|
(input)="applyFilter()"
|
||||||
|
matTooltip="Filter the items of the basket from the table"
|
||||||
|
[(ngModel)]="filterText"/>
|
||||||
|
<button mat-button matSuffix mat-icon-button
|
||||||
|
*ngIf="filterText"
|
||||||
|
aria-label="Clear"
|
||||||
|
(click)="filterText='';applyFilter()"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
<gisaf-admin-basket-upload [basket]='basket' [dataSource]='dataSource'>
|
||||||
|
</gisaf-admin-basket-upload>
|
||||||
|
</div>
|
||||||
|
<table mat-table matSort [dataSource]="dataSource" class='content'>
|
||||||
|
<ng-container matColumnDef="delete">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
|
<form [formGroup]="unlockDeleteFormGroup">
|
||||||
|
<mat-checkbox formControlName='canDelete' matTooltip='Unlock delete buttons'>
|
||||||
|
</mat-checkbox>
|
||||||
|
</form>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let item">
|
||||||
|
<button mat-button (click)="deleteItem(item)" matTooltip="Delete"
|
||||||
|
[disabled]="!unlockDeleteFormGroup.controls['canDelete'].value">
|
||||||
|
<mat-icon aria-label="delete">delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="import">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Import</th>
|
||||||
|
<td mat-cell *matCellDef="let item">
|
||||||
|
<button mat-button (click)="importItem(item, $event.ctrlKey)"
|
||||||
|
matTooltip="Import to the database. Press 'Control' key for a dry run (no actual change will happen)"
|
||||||
|
>
|
||||||
|
<mat-icon aria-label="import">input</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
|
||||||
|
<td mat-cell *matCellDef="let item">
|
||||||
|
<span (click)="download(item)" [title]="item.dir">{{ item.name }}</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="store">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Store</th>
|
||||||
|
<td mat-cell *matCellDef="let item">{{ item.store }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="time">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Import time</th>
|
||||||
|
<td mat-cell *matCellDef="let item">{{ isDate(item.time) ? (item.time | date:'dd/MM/yyyy, HH:mm:ss') : 'never' }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="url">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>URL</th>
|
||||||
|
<td mat-cell *matCellDef="let item">{{ item.url }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="status">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Status</th>
|
||||||
|
<td mat-cell *matCellDef="let item">{{ item.status }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="project">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Project</th>
|
||||||
|
<td mat-cell *matCellDef="let item">{{ item.project }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="surveyor">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Surveyor</th>
|
||||||
|
<td mat-cell *matCellDef="let item">{{ item.surveyor }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="equipment">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Equipment</th>
|
||||||
|
<td mat-cell *matCellDef="let item">{{ item.equipment }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="getColumns();sticky:true"></tr>
|
||||||
|
<tr mat-row [ngClass]="{even: even}"
|
||||||
|
*matRowDef="let row; let even = even; columns:getColumns()"
|
||||||
|
></tr>
|
||||||
|
</table>
|
||||||
|
<!--
|
||||||
|
<gisaf-admin-basket-item [file]='file' *ngFor='let file of basket.file'>
|
||||||
|
</gisaf-admin-basket-item>
|
||||||
|
-->
|
||||||
|
<mat-paginator
|
||||||
|
class="paginator"
|
||||||
|
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||||
|
[pageIndex]='0'
|
||||||
|
[pageSize]='10'
|
||||||
|
showFirstLastButtons
|
||||||
|
>
|
||||||
|
</mat-paginator>
|
||||||
|
</div>
|
107
src/app/admin/admin-basket/basket.component.ts
Normal file
107
src/app/admin/admin-basket/basket.component.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { Component, OnInit, Input, ViewChild, ElementRef,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
|
||||||
|
import { ActivatedRoute } from '@angular/router'
|
||||||
|
import { UntypedFormGroup, UntypedFormControl } from '@angular/forms'
|
||||||
|
|
||||||
|
import { SelectionModel } from '@angular/cdk/collections'
|
||||||
|
import { MatPaginator } from '@angular/material/paginator'
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar'
|
||||||
|
import { MatSort } from '@angular/material/sort'
|
||||||
|
import { MatTableDataSource } from '@angular/material/table'
|
||||||
|
|
||||||
|
import { AdminDataService } from '../admin-data.service'
|
||||||
|
import { AdminBasketDataService, AdminBasket, AdminBasketFile } from './data.service'
|
||||||
|
import { HtmlSnackbarComponent } from '../../custom-snackbar/custom-snackbar.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-basket',
|
||||||
|
templateUrl: './basket.component.html',
|
||||||
|
styleUrls: ['./basket.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AdminBasketComponent implements OnInit {
|
||||||
|
constructor(
|
||||||
|
public adminDataService: AdminDataService,
|
||||||
|
public adminBasketDataService: AdminBasketDataService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
basket: AdminBasket
|
||||||
|
dataSource: MatTableDataSource<object>
|
||||||
|
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator
|
||||||
|
@ViewChild(MatSort, {static: true}) sort: MatSort
|
||||||
|
selection = new SelectionModel(true, [])
|
||||||
|
unlockDeleteFormGroup: UntypedFormGroup = new UntypedFormGroup({})
|
||||||
|
columns: string[] = [
|
||||||
|
'name',
|
||||||
|
'status',
|
||||||
|
'time',
|
||||||
|
'store',
|
||||||
|
'project',
|
||||||
|
'surveyor',
|
||||||
|
'equipment',
|
||||||
|
'import',
|
||||||
|
'delete',
|
||||||
|
]
|
||||||
|
filterText: string
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.route.data.subscribe(
|
||||||
|
(basket: object) => {
|
||||||
|
this.basket = basket['basket']
|
||||||
|
this.dataSource = new MatTableDataSource(this.basket.files)
|
||||||
|
this.dataSource.sort = this.sort
|
||||||
|
this.dataSource.paginator = this.paginator
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.unlockDeleteFormGroup = new UntypedFormGroup({
|
||||||
|
'canDelete': new UntypedFormControl(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getColumns() {
|
||||||
|
return this.columns.filter(
|
||||||
|
col => this.basket.columns.indexOf(col) != -1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter() {
|
||||||
|
this.dataSource.filter = this.filterText.trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
download(item: AdminBasketFile) {
|
||||||
|
window.open('/download/basket/' + this.basket.name + '/' + item.id + '/' + item.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
importItem(item: AdminBasketFile, dryRun: boolean) {
|
||||||
|
this.adminBasketDataService.importItem(this.basket.name, item.id, dryRun).subscribe(
|
||||||
|
resp => {
|
||||||
|
this.basket.files.find(row => row.id == item.id).time = new Date(resp.time)
|
||||||
|
this.snackBar.openFromComponent(HtmlSnackbarComponent, {
|
||||||
|
data: resp,
|
||||||
|
//duration: 3000
|
||||||
|
})
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteItem(item: AdminBasketFile) {
|
||||||
|
this.adminBasketDataService.deleteItem(this.basket.name, item.id).subscribe(
|
||||||
|
id => {
|
||||||
|
let dsi = this.dataSource.data.findIndex(fi => fi['id'] == id)
|
||||||
|
this.dataSource.data.splice(dsi, 1)
|
||||||
|
// Force Angular change detection (??)
|
||||||
|
this.dataSource.data = this.dataSource.data
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isDate(val: any) {
|
||||||
|
return val instanceof Date && isFinite(<any>val)
|
||||||
|
}
|
||||||
|
}
|
223
src/app/admin/admin-basket/data.service.ts
Normal file
223
src/app/admin/admin-basket/data.service.ts
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { Apollo, gql } from 'apollo-angular'
|
||||||
|
|
||||||
|
import { Project } from '../admin-data.service'
|
||||||
|
|
||||||
|
export class AdminBasketFile {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public dir: string,
|
||||||
|
public name: string,
|
||||||
|
public url: string,
|
||||||
|
public md5: string,
|
||||||
|
public time: Date,
|
||||||
|
public comment: string,
|
||||||
|
public status: string,
|
||||||
|
public store: string,
|
||||||
|
public project: string,
|
||||||
|
public surveyor: string,
|
||||||
|
public equipment: string,
|
||||||
|
public import_result?: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminBasket {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public files?: AdminBasketFile[],
|
||||||
|
public columns?: string[],
|
||||||
|
public uploadFields?: string[],
|
||||||
|
public projects?: Project[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BasketImportResult {
|
||||||
|
constructor(
|
||||||
|
public time: Date,
|
||||||
|
public message: string,
|
||||||
|
public details?: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminBasketUploadFieldData {
|
||||||
|
constructor(
|
||||||
|
public stores: string[],
|
||||||
|
public statuses: string[],
|
||||||
|
public projects: string[],
|
||||||
|
public surveyors: string[],
|
||||||
|
public equipments: string[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAdminBasketsQuery = gql`
|
||||||
|
query admin_baskets {
|
||||||
|
admin_baskets {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const getAdminBasketUploadFieldDataQuery = gql`
|
||||||
|
query admin_basket_upload_field_data {
|
||||||
|
admin_basket_upload_field_data {
|
||||||
|
store
|
||||||
|
status
|
||||||
|
project
|
||||||
|
surveyor
|
||||||
|
equipment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const getAdminBasketQuery = gql`
|
||||||
|
query admin_basket ($name: String!) {
|
||||||
|
admin_basket (name: $name) {
|
||||||
|
name
|
||||||
|
files {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
dir
|
||||||
|
url
|
||||||
|
md5
|
||||||
|
time
|
||||||
|
comment
|
||||||
|
status
|
||||||
|
store
|
||||||
|
project
|
||||||
|
surveyor
|
||||||
|
equipment
|
||||||
|
}
|
||||||
|
columns
|
||||||
|
uploadFields
|
||||||
|
projects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const deleteAdminBasketItemMutation = gql`
|
||||||
|
mutation deleteBasketItem ($basket: String!, $id: Int!) {
|
||||||
|
deleteBasketItem (basket: $basket, id: $id) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const importAdminBasketItemMutation = gql`
|
||||||
|
mutation importBasketItem ($basket: String!, $id: Int!, $dryRun: Boolean) {
|
||||||
|
importBasketItem (basket: $basket, id: $id, dryRun: $dryRun) {
|
||||||
|
result {
|
||||||
|
message
|
||||||
|
time
|
||||||
|
details
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminBasketDataService {
|
||||||
|
constructor(
|
||||||
|
private apollo: Apollo,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getBaskets(): Observable<AdminBasket[]> {
|
||||||
|
// Get the list a basket names
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getAdminBasketsQuery,
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['admin_baskets'].map(
|
||||||
|
(data: object) => new AdminBasket(
|
||||||
|
data['name'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
getBasketUploadFieldData(): Observable<AdminBasketUploadFieldData> {
|
||||||
|
// Get the list a basket names
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getAdminBasketUploadFieldDataQuery,
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['admin_basket_upload_field_data'].map(
|
||||||
|
(data: object) => new AdminBasketUploadFieldData(
|
||||||
|
data['store'],
|
||||||
|
data['status'],
|
||||||
|
data['project'],
|
||||||
|
data['surveyor'],
|
||||||
|
data['equipment'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
getBasket(name: string): Observable<AdminBasket> {
|
||||||
|
// Get all info and content of a basket
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getAdminBasketQuery,
|
||||||
|
variables: {
|
||||||
|
name: name
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => {
|
||||||
|
let data = res['data']['admin_basket']
|
||||||
|
return new AdminBasket(
|
||||||
|
data['name'],
|
||||||
|
data['files'].map(file => new AdminBasketFile(
|
||||||
|
file['id'],
|
||||||
|
file['dir'],
|
||||||
|
file['name'],
|
||||||
|
file['url'],
|
||||||
|
file['md5'],
|
||||||
|
new Date(file['time']),
|
||||||
|
file['comment'],
|
||||||
|
file['status'],
|
||||||
|
file['store'],
|
||||||
|
file['project'],
|
||||||
|
file['surveyor'],
|
||||||
|
file['equipment'],
|
||||||
|
)),
|
||||||
|
data['columns'],
|
||||||
|
data['uploadFields'],
|
||||||
|
// XXX: the proejct id isn't actually used in the UI,
|
||||||
|
// but required in class definition
|
||||||
|
data['projects'] && data['projects'].map(
|
||||||
|
(projectName: string) => new Project(undefined, projectName)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
importItem(basket: string, id: number, dryRun: boolean=false): Observable<BasketImportResult> {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: importAdminBasketItemMutation,
|
||||||
|
variables: {
|
||||||
|
basket: basket,
|
||||||
|
id: id,
|
||||||
|
dryRun: dryRun
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
resp => new BasketImportResult(
|
||||||
|
resp['data']['importBasketItem']['result']['time'],
|
||||||
|
resp['data']['importBasketItem']['result']['message'],
|
||||||
|
JSON.parse(resp['data']['importBasketItem']['result']['details']),
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteItem(basket: string, id: number) {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: deleteAdminBasketItemMutation,
|
||||||
|
variables: {
|
||||||
|
basket: basket,
|
||||||
|
id: id,
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
resp => resp['data']['deleteBasketItem']['result']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
125
src/app/admin/admin-data.service.ts
Normal file
125
src/app/admin/admin-data.service.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import { Injectable, Input } from '@angular/core'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { Apollo, gql } from 'apollo-angular'
|
||||||
|
|
||||||
|
import { Store } from './admin-manage/data.service'
|
||||||
|
|
||||||
|
export class Project {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public name: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Surveyor {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public name: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Equipment {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public name: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SurveyMeta {
|
||||||
|
constructor(
|
||||||
|
public projects: Project[],
|
||||||
|
public surveyors: Surveyor[],
|
||||||
|
public equipments: Equipment[],
|
||||||
|
public statuses: string[],
|
||||||
|
public stores_misc: Store[],
|
||||||
|
public stores_line_work: Store[],
|
||||||
|
public defaults: Object
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSurveyMeta = gql`
|
||||||
|
query survey_meta {
|
||||||
|
survey_meta {
|
||||||
|
projects {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
surveyors {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
equipments {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
stores_misc {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
stores_line_work {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
statuses
|
||||||
|
default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const admin_models_menu_bar_query = gql`
|
||||||
|
query admin_models_menu_bar {
|
||||||
|
admin_models_menu_bar{
|
||||||
|
name
|
||||||
|
items{
|
||||||
|
name
|
||||||
|
module
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminDataService {
|
||||||
|
surveyMeta: SurveyMeta
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private apollo: Apollo,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getModelsMenuBar(): Observable<object[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: admin_models_menu_bar_query
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['admin_models_menu_bar_query']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
getSurveyMeta(): Observable<SurveyMeta> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getSurveyMeta,
|
||||||
|
}).pipe(map(
|
||||||
|
resp => {
|
||||||
|
let data = resp['data']['survey_meta']
|
||||||
|
this.surveyMeta = new SurveyMeta(
|
||||||
|
data['projects'].map(
|
||||||
|
item => new Project(item['id'], item['name'])
|
||||||
|
).sort((i, j) => i.name > j.name ? 1 : -1),
|
||||||
|
data['surveyors'].map(
|
||||||
|
item => new Surveyor(item['id'], item['name'])
|
||||||
|
).sort((i, j) => i.name > j.name ? 1 : -1),
|
||||||
|
data['equipments'].map(
|
||||||
|
item => new Equipment(item['id'], item['name'])
|
||||||
|
).sort((i, j) => i.name > j.name ? 1 : -1),
|
||||||
|
data['statuses'],
|
||||||
|
data['stores_misc'].map(
|
||||||
|
item => new Store(item['name'])
|
||||||
|
).sort((i, j) => i.name > j.name ? 1 : -1),
|
||||||
|
data['stores_line_work'].map(
|
||||||
|
item => new Store(item['name'])
|
||||||
|
).sort((i, j) => i.name > j.name ? 1 : -1),
|
||||||
|
JSON.parse(data['default']),
|
||||||
|
)
|
||||||
|
return this.surveyMeta
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
31
src/app/admin/admin-detail/admin-detail-resolver.service.ts
Normal file
31
src/app/admin/admin-detail/admin-detail-resolver.service.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { ModelDataService } from '../../_services/apollo.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DetailResolver implements Resolve<object> {
|
||||||
|
constructor(
|
||||||
|
private modelDataService: ModelDataService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<object> {
|
||||||
|
var pk = route.paramMap.get('pk')
|
||||||
|
var modelName = route.paramMap.get('modelName')
|
||||||
|
|
||||||
|
return this.modelDataService.resolveItemData(modelName, pk).pipe(map(
|
||||||
|
res => {
|
||||||
|
if (!res) {
|
||||||
|
this.router.navigate(['/admin', modelName])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
58
src/app/admin/admin-detail/admin-detail.component.css
Normal file
58
src/app/admin/admin-detail/admin-detail.component.css
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*@import '../node_modules/@angular/material/prebuilt-themes/purple-green.css';*/
|
||||||
|
|
||||||
|
/*TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version.*/
|
||||||
|
mat-card {
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version.*/
|
||||||
|
mat-card .form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version.*/
|
||||||
|
mat-card .form>* {
|
||||||
|
/*display: block;*/
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version.*/
|
||||||
|
mat-card-footer {
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version.*/
|
||||||
|
mat-card-footer > button {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form>div>* {
|
||||||
|
margin: 0 1em;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
mat-form-field.field-symbol input {
|
||||||
|
font-family: GisafSymbols;
|
||||||
|
font-size: 200%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field.field-symbol {
|
||||||
|
width: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field.field-id {
|
||||||
|
width: 4em;
|
||||||
|
}
|
52
src/app/admin/admin-detail/admin-detail.component.html
Normal file
52
src/app/admin/admin-detail/admin-detail.component.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<mat-card appearance="outlined">
|
||||||
|
<mat-card-title>
|
||||||
|
{{ modelName }} #{{ pk }}
|
||||||
|
</mat-card-title>
|
||||||
|
<mat-card-content *ngIf="model">
|
||||||
|
<div [formGroup]="formGroup" class='form'>
|
||||||
|
<ng-container *ngFor="let field of fields">
|
||||||
|
<mat-checkbox
|
||||||
|
matInput
|
||||||
|
*ngIf="field.isCheckbox"
|
||||||
|
[formControlName]="field.name"
|
||||||
|
>
|
||||||
|
{{ field.name }}
|
||||||
|
</mat-checkbox>
|
||||||
|
<mat-form-field *ngIf="field.isTextArea">
|
||||||
|
<mat-label>{{ field.name }}</mat-label>
|
||||||
|
<textarea matInput
|
||||||
|
[formControlName]="field.name"
|
||||||
|
>
|
||||||
|
</textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field
|
||||||
|
*ngIf="!field.isTextArea && !field.isCheckbox"
|
||||||
|
[class]="'field-' + field.name"
|
||||||
|
>
|
||||||
|
<mat-label>{{ field.name }}</mat-label>
|
||||||
|
<input matInput
|
||||||
|
[type]="field.inputType"
|
||||||
|
[formControlName]="field.name"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngFor="let join of model.inspection.joins">
|
||||||
|
<join-select
|
||||||
|
[join]='join'
|
||||||
|
[formGroup]='formGroup'
|
||||||
|
>
|
||||||
|
</join-select>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-footer>
|
||||||
|
<button mat-raised-button color='primary' (click)="submit()" [disabled]="!formGroup.valid">
|
||||||
|
<mat-icon aria-label="submit">done</mat-icon>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
<button mat-raised-button routerLink="..">
|
||||||
|
<mat-icon aria-label="cancel">cancel</mat-icon>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</mat-card-footer>
|
||||||
|
</mat-card>
|
88
src/app/admin/admin-detail/admin-detail.component.ts
Normal file
88
src/app/admin/admin-detail/admin-detail.component.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import { Component, Input, ViewChild, OnInit, HostBinding,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
|
||||||
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router'
|
||||||
|
import { UntypedFormGroup, FormControl } from '@angular/forms'
|
||||||
|
|
||||||
|
import { fadeInAnimation } from '../../animations'
|
||||||
|
import {
|
||||||
|
ModelDataService,
|
||||||
|
Model,
|
||||||
|
FieldIntrospection,
|
||||||
|
} from '../../_services/apollo.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-detail',
|
||||||
|
templateUrl: './admin-detail.component.html',
|
||||||
|
styleUrls: ['./admin-detail.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [ fadeInAnimation ]
|
||||||
|
})
|
||||||
|
|
||||||
|
export class AdminDetailComponent implements OnInit {
|
||||||
|
/*
|
||||||
|
@HostBinding('@fadeInAnimation') fadeInAnimation = true
|
||||||
|
@HostBinding('style.display') display = 'block'
|
||||||
|
@HostBinding('style.position') position = 'absolute'
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Input() modelName: string
|
||||||
|
@Input() pk: string
|
||||||
|
model: Model
|
||||||
|
item: object
|
||||||
|
formGroup: UntypedFormGroup
|
||||||
|
fields: FieldIntrospection[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected router: Router,
|
||||||
|
public modelDataService: ModelDataService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Set an empty formGroup
|
||||||
|
this.formGroup = new UntypedFormGroup({})
|
||||||
|
// In case i'm initiated with the route resolver:
|
||||||
|
// TODO: use secondary route
|
||||||
|
this.route.data.subscribe(
|
||||||
|
(data) => {
|
||||||
|
if (Object.keys(data).length != 0) {
|
||||||
|
this.route.params.subscribe(
|
||||||
|
params => {
|
||||||
|
let item = data['item']
|
||||||
|
// FIXME (with secondary route)
|
||||||
|
/*
|
||||||
|
this.setItem(
|
||||||
|
params['modelName'],
|
||||||
|
item['model'],
|
||||||
|
item['item'][params['modelName']][0],
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(modelName, model, item) {
|
||||||
|
// Fill me with the provided model and item data
|
||||||
|
// Can be called from the init and route resolver or manually (map, etc)
|
||||||
|
this.formGroup = new UntypedFormGroup({})
|
||||||
|
this.modelName = modelName
|
||||||
|
this.item = item
|
||||||
|
this.model = model
|
||||||
|
// Get the form fields and build the formGroup controls
|
||||||
|
this.fields = this.model.getFormFields(this.formGroup, item)
|
||||||
|
this.pk = this.item[this.model.introspection.pkFields()[0].name]
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.modelDataService.mutate(this.model.introspection, this.formGroup.value).subscribe(
|
||||||
|
res => {
|
||||||
|
console.log('TODO: submit', res)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
40
src/app/admin/admin-detail/admin-detail.module.ts
Normal file
40
src/app/admin/admin-detail/admin-detail.module.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms'
|
||||||
|
|
||||||
|
import { MatButtonModule } from '@angular/material/button'
|
||||||
|
import { MatCardModule } from '@angular/material/card'
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox'
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field'
|
||||||
|
import { MatIconModule } from '@angular/material/icon'
|
||||||
|
import { MatInputModule } from '@angular/material/input'
|
||||||
|
import { MatSelectModule } from '@angular/material/select'
|
||||||
|
|
||||||
|
import { JoinSelectComponent } from './join-select.component'
|
||||||
|
|
||||||
|
import { AdminDetailComponent } from './admin-detail.component'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
|
||||||
|
ReactiveFormsModule,
|
||||||
|
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AdminDetailComponent,
|
||||||
|
JoinSelectComponent
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AdminDetailComponent,
|
||||||
|
JoinSelectComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminDetailModule { }
|
14
src/app/admin/admin-detail/join-select.component.html
Normal file
14
src/app/admin/admin-detail/join-select.component.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<mat-form-field [formGroup]="formGroup">
|
||||||
|
<mat-label>{{ join.name }}</mat-label>
|
||||||
|
<mat-select
|
||||||
|
[title]="join.name"
|
||||||
|
[formControlName]="join.name"
|
||||||
|
>
|
||||||
|
<mat-option
|
||||||
|
*ngFor='let choice of choices'
|
||||||
|
[value]="choice['id']"
|
||||||
|
>
|
||||||
|
{{ choice['name'] }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
42
src/app/admin/admin-detail/join-select.component.ts
Normal file
42
src/app/admin/admin-detail/join-select.component.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { Component, Input, ViewChild, OnInit,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
|
||||||
|
|
||||||
|
import { UntypedFormGroup, FormControl } from '@angular/forms'
|
||||||
|
|
||||||
|
import { map, switchMap } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { gql } from 'apollo-angular'
|
||||||
|
|
||||||
|
import { ModelDataService, JoinField } from '../../_services/apollo.service'
|
||||||
|
import { Relation } from '../models'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'join-select',
|
||||||
|
templateUrl: './join-select.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class JoinSelectComponent implements OnInit {
|
||||||
|
@Input() join: JoinField
|
||||||
|
@Input() formGroup: UntypedFormGroup
|
||||||
|
choices: object[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected modelDataService: ModelDataService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
var modelName = this.join.target.split('.').pop()
|
||||||
|
let query = gql`query model {${modelName}{name id}}`
|
||||||
|
this.modelDataService.get(query).pipe(map(data => {
|
||||||
|
return data[modelName]
|
||||||
|
})).subscribe(data => {
|
||||||
|
this.choices = data.map(
|
||||||
|
item => {
|
||||||
|
return {id: +item['id'], name: item['name']}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
1
src/app/admin/admin-home/admin-home.component.css
Normal file
1
src/app/admin/admin-home/admin-home.component.css
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/*@import '../node_modules/@angular/material/prebuilt-themes/purple-green.css';*/
|
9
src/app/admin/admin-home/admin-home.component.html
Normal file
9
src/app/admin/admin-home/admin-home.component.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<mat-card appearance="outlined">
|
||||||
|
<mat-card-title>Gisaf admin/control center</mat-card-title>
|
||||||
|
<mat-card-content>
|
||||||
|
<p>
|
||||||
|
This is the adminstration area: baskets for importing files,
|
||||||
|
tools for the management of the database...
|
||||||
|
</p>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
9
src/app/admin/admin-home/admin-home.component.ts
Normal file
9
src/app/admin/admin-home/admin-home.component.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Component } from '@angular/core'
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-home',
|
||||||
|
templateUrl: './admin-home.component.html',
|
||||||
|
styleUrls: ['./admin-home.component.css']
|
||||||
|
})
|
||||||
|
export class AdminHomeComponent {}
|
77
src/app/admin/admin-list/admin-list.component.css
Normal file
77
src/app/admin/admin-list/admin-list.component.css
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*@import '../node_modules/@angular/material/prebuilt-themes/purple-green.css';*/
|
||||||
|
|
||||||
|
/*TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version.*/
|
||||||
|
mat-card {
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-header-cell {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mat-mdc-cell:first-child, th.mat-mdc-header-cell:first-child {
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mat-mdc-cell, th.mat-mdc-header-cell {
|
||||||
|
border-left: 1px solid rgba(255,255,255,.12);
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mat-mdc-cell:last-child, th.mat-mdc-header-cell:last-child {
|
||||||
|
border-right: 1px solid #8080802b;
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.mat-mdc-header-row {
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.mat-mdc-row {
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-column-select {
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-column-actions {
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mat-column-symbol>div {
|
||||||
|
font-family: GisafSymbols;
|
||||||
|
font-size: 200%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version.*/
|
||||||
|
mat-card-footer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version.*/
|
||||||
|
mat-card-footer .actions {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version.*/
|
||||||
|
mat-card-footer .actions {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version.*/
|
||||||
|
mat-card-footer .filter {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mat-mdc-cell>div {
|
||||||
|
max-height: 5em;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
79
src/app/admin/admin-list/admin-list.component.html
Normal file
79
src/app/admin/admin-list/admin-list.component.html
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<mat-card appearance="outlined">
|
||||||
|
<mat-card-title *ngIf="model">{{ model.name }}</mat-card-title>
|
||||||
|
<mat-card-content>
|
||||||
|
<table mat-table
|
||||||
|
matSort
|
||||||
|
*ngIf="model"
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
>
|
||||||
|
<ng-container matColumnDef="select">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
|
<mat-checkbox (change)="$event ? masterToggle() : null"
|
||||||
|
[checked]="(selection.selected.length > 0) && isAllSelected()"
|
||||||
|
[indeterminate]="(selection.selected.length > 0) && !isAllSelected()">
|
||||||
|
</mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let row">
|
||||||
|
<mat-checkbox (click)="$event.stopPropagation()"
|
||||||
|
(change)="$event ? selection.toggle(row) : null"
|
||||||
|
[checked]="selection.isSelected(row)">
|
||||||
|
</mat-checkbox>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let row">
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)='showDetail(row)'>
|
||||||
|
<mat-icon aria-label="edit">edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container
|
||||||
|
*ngFor="let colName of model.columns()"
|
||||||
|
[matColumnDef]="colName">
|
||||||
|
<th mat-header-cell mat-sort-header *matHeaderCellDef>{{ colName }}</th>
|
||||||
|
<td mat-cell *matCellDef="let element">
|
||||||
|
<div>
|
||||||
|
{{ element[colName] }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="allColumns; sticky: true"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: allColumns;"
|
||||||
|
(click)="showDetail(row)"></tr>
|
||||||
|
</table>
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-footer>
|
||||||
|
<div class="actions">
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
(click)='add()'
|
||||||
|
>
|
||||||
|
<mat-icon aria-label="add">add</mat-icon>
|
||||||
|
Add new
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
(click)='deleteSelected()'
|
||||||
|
>
|
||||||
|
<mat-icon aria-label="delete">delete</mat-icon>
|
||||||
|
Delete selected
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<mat-form-field class="filter">
|
||||||
|
<mat-label>Filter</mat-label>
|
||||||
|
<input matInput (keyup)="applyFilter($event.target)">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-paginator
|
||||||
|
class="paginator"
|
||||||
|
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||||
|
[pageIndex]='0'
|
||||||
|
[pageSize]='10'
|
||||||
|
showFirstLastButtons
|
||||||
|
>
|
||||||
|
</mat-paginator>
|
||||||
|
</mat-card-footer>
|
||||||
|
</mat-card>
|
25
src/app/admin/admin-list/admin-list.component.spec.ts
Normal file
25
src/app/admin/admin-list/admin-list.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AdminListComponent } from './admin-list.component';
|
||||||
|
|
||||||
|
describe('AdminListComponent', () => {
|
||||||
|
let component: AdminListComponent;
|
||||||
|
let fixture: ComponentFixture<AdminListComponent>;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ AdminListComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdminListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
121
src/app/admin/admin-list/admin-list.component.ts
Normal file
121
src/app/admin/admin-list/admin-list.component.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import { Component, Input, ViewChild, OnInit, HostBinding,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
|
||||||
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router'
|
||||||
|
|
||||||
|
import { SelectionModel } from '@angular/cdk/collections'
|
||||||
|
import { MatPaginator } from '@angular/material/paginator'
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar'
|
||||||
|
import { MatSort } from '@angular/material/sort'
|
||||||
|
import { MatTableDataSource } from '@angular/material/table'
|
||||||
|
|
||||||
|
import { map, switchMap } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { slideInDownAnimation } from '../../animations'
|
||||||
|
|
||||||
|
import { ModelDataService, ModelIntrospection, FieldIntrospection } from '../../_services/apollo.service'
|
||||||
|
//import { TableDataSource } from './datasource'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-list',
|
||||||
|
templateUrl: './admin-list.component.html',
|
||||||
|
styleUrls: ['./admin-list.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [ slideInDownAnimation ]
|
||||||
|
})
|
||||||
|
export class AdminListComponent implements OnInit {
|
||||||
|
/*
|
||||||
|
@HostBinding('@slideInDownAnimation') slideInDownAnimation = true
|
||||||
|
@HostBinding('style.display') display = 'block'
|
||||||
|
@HostBinding('style.position') position = 'absolute'
|
||||||
|
*/
|
||||||
|
|
||||||
|
dataSource: MatTableDataSource<object>
|
||||||
|
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator
|
||||||
|
@ViewChild(MatSort, {static: true}) sort: MatSort
|
||||||
|
model: ModelIntrospection
|
||||||
|
allColumns: string[] = []
|
||||||
|
pageIndex: number
|
||||||
|
pageSize: number
|
||||||
|
selection = new SelectionModel(true, [])
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected router: Router,
|
||||||
|
protected dataService: ModelDataService,
|
||||||
|
public snackBar: MatSnackBar,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.route.params.subscribe(params => {
|
||||||
|
this.setModel(params['modelName'])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setModel(modelName) {
|
||||||
|
if (!modelName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.dataService.introspect(modelName).subscribe(
|
||||||
|
res => {
|
||||||
|
this.model = res
|
||||||
|
this.allColumns = ['select', 'actions'].concat(this.model.columns())
|
||||||
|
this.getData()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
this.dataService.all(this.model).pipe(map(data => {
|
||||||
|
return data[this.model.name]
|
||||||
|
})).subscribe({
|
||||||
|
next: data => {
|
||||||
|
if (data) {
|
||||||
|
this.dataSource = new MatTableDataSource(data)
|
||||||
|
this.dataSource.paginator = this.paginator
|
||||||
|
this.dataSource.sort = this.sort
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.dataSource = new MatTableDataSource([])
|
||||||
|
}
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
this.snackBar.open(err, 'close', {duration: 3000})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the number of selected elements matches the total number of rows. */
|
||||||
|
isAllSelected() {
|
||||||
|
const numSelected = this.selection.selected.length
|
||||||
|
const numRows = this.dataSource.data.length
|
||||||
|
return numSelected === numRows
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Selects all rows if they are not all selected; otherwise clear selection. */
|
||||||
|
masterToggle() {
|
||||||
|
this.isAllSelected() ?
|
||||||
|
this.selection.clear() :
|
||||||
|
this.dataSource.data.forEach(row => this.selection.select(row))
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter(target: EventTarget) {
|
||||||
|
let filterValue = target['value']
|
||||||
|
this.dataSource.filter = filterValue.trim().toLowerCase()
|
||||||
|
|
||||||
|
if (this.dataSource.paginator) {
|
||||||
|
this.dataSource.paginator.firstPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showDetail(item) {
|
||||||
|
this.router.navigate(['/admin/model', this.model.name, item[this.model.pkFields()[0].name]])
|
||||||
|
}
|
||||||
|
|
||||||
|
add() {
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSelected() {
|
||||||
|
}
|
||||||
|
}
|
404
src/app/admin/admin-manage/access/access-data.service.ts
Normal file
404
src/app/admin/admin-manage/access/access-data.service.ts
Normal file
|
@ -0,0 +1,404 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
|
||||||
|
import { Observable, BehaviorSubject, forkJoin } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { Apollo, gql } from 'apollo-angular'
|
||||||
|
|
||||||
|
import { MatDialog } from '@angular/material/dialog'
|
||||||
|
|
||||||
|
import { GisafAdminAccessRoleDialogComponent } from './role-dialog.component'
|
||||||
|
import { GisafAdminAccessUserDialogComponent } from './user-dialog.component'
|
||||||
|
import { Role, User, ACL } from './models'
|
||||||
|
|
||||||
|
|
||||||
|
const getUsersQuery = gql`
|
||||||
|
query users {
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
email
|
||||||
|
active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const getRolesQuery = gql`
|
||||||
|
query getRoles {
|
||||||
|
roles {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const getAclsQuery = gql`
|
||||||
|
query acls {
|
||||||
|
acls {
|
||||||
|
user_id
|
||||||
|
role_ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const addUserRoleMutation = gql`
|
||||||
|
mutation addUserRole(
|
||||||
|
$user_id: Int!,
|
||||||
|
$role_id: Int!,
|
||||||
|
) {
|
||||||
|
addUserRole(
|
||||||
|
user_id: $user_id,
|
||||||
|
role_id: $role_id,
|
||||||
|
) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const deleteUserRoleMutation = gql`
|
||||||
|
mutation deleteUserRole(
|
||||||
|
$user_id: Int!,
|
||||||
|
$role_id: Int!,
|
||||||
|
) {
|
||||||
|
deleteUserRole(
|
||||||
|
user_id: $user_id,
|
||||||
|
role_id: $role_id,
|
||||||
|
) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const deleteUserMutation = gql`
|
||||||
|
mutation deleteUser(
|
||||||
|
$id: Int!,
|
||||||
|
) {
|
||||||
|
deleteUser(
|
||||||
|
id: $id,
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const deleteRoleMutation = gql`
|
||||||
|
mutation deleteRole(
|
||||||
|
$id: Int!,
|
||||||
|
) {
|
||||||
|
deleteRole(
|
||||||
|
id: $id,
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const saveRoleMutation = gql`
|
||||||
|
mutation saveRole(
|
||||||
|
$id: Int,
|
||||||
|
$name: String!,
|
||||||
|
$description: String!
|
||||||
|
) {
|
||||||
|
saveRole (
|
||||||
|
id: $id,
|
||||||
|
name: $name,
|
||||||
|
description: $description
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const saveUserMutation = gql`
|
||||||
|
mutation saveUser(
|
||||||
|
$id: Int,
|
||||||
|
$name: String!,
|
||||||
|
$email: String!,
|
||||||
|
$password: String,
|
||||||
|
$active: Boolean!,
|
||||||
|
) {
|
||||||
|
saveUser (
|
||||||
|
id: $id,
|
||||||
|
name: $name,
|
||||||
|
email: $email,
|
||||||
|
active: $active,
|
||||||
|
password: $password
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminManageAccessDataService {
|
||||||
|
constructor(
|
||||||
|
private apollo: Apollo,
|
||||||
|
public dialog: MatDialog,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private _users = new BehaviorSubject<User[]>([])
|
||||||
|
private _roles = new BehaviorSubject<Role[]>([])
|
||||||
|
private _acls = new BehaviorSubject<ACL[]>([])
|
||||||
|
users$: Observable<User[]> = this._users.asObservable()
|
||||||
|
roles$: Observable<Role[]> = this._roles.asObservable()
|
||||||
|
acls$: Observable<ACL[]> = this._acls.asObservable()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.getAllAccessData().subscribe()
|
||||||
|
this.acls$.subscribe(
|
||||||
|
(acls: ACL[]) => {
|
||||||
|
/*
|
||||||
|
acls.forEach(
|
||||||
|
(acl: ACL) => {
|
||||||
|
// Scan all the list of users, should be an object
|
||||||
|
let user: User = this._users.value.find(u=>u.id==acl.user_id)
|
||||||
|
user._roles.next(acl.role_ids.map(
|
||||||
|
role_id => this._roles.value.find(
|
||||||
|
(role: Role) => role.id == role_id
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
this._users.value.forEach(
|
||||||
|
(user: User) => {
|
||||||
|
let acl: ACL = acls.find(a => a.user_id == user.id)
|
||||||
|
// Scan all the list of roles, should be an object (optimize)
|
||||||
|
if (acl && acl.role_ids) {
|
||||||
|
user._roles.next(acl.role_ids.map(
|
||||||
|
role_id => this._roles.value.find(
|
||||||
|
(role: Role) => role.id == role_id
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
user._roles.next([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsers(): Observable<User[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getUsersQuery,
|
||||||
|
}).pipe(map(
|
||||||
|
res =>
|
||||||
|
res['data']['users'].map(
|
||||||
|
// Really create an object because User has roles in its constructor
|
||||||
|
(item: object) => new User(
|
||||||
|
item['id'],
|
||||||
|
item['name'],
|
||||||
|
item['email'],
|
||||||
|
item['active'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoles(): Observable<Role[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getRolesQuery,
|
||||||
|
}).pipe(map(
|
||||||
|
res =>
|
||||||
|
res['data']['roles'].map(
|
||||||
|
(item: object) => item as Role
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getACLs(): Observable<ACL[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getAclsQuery,
|
||||||
|
}).pipe(map(
|
||||||
|
res =>
|
||||||
|
res['data']['acls'].map(
|
||||||
|
(item: object) => item as ACL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllAccessData(): Observable<void> {
|
||||||
|
return forkJoin([
|
||||||
|
this.getUsers(),
|
||||||
|
this.getRoles(),
|
||||||
|
this.getACLs(),
|
||||||
|
]).pipe(map(
|
||||||
|
([users, roles, acls]) => {
|
||||||
|
this._users.next(users)
|
||||||
|
this._roles.next(roles)
|
||||||
|
this._acls.next(acls)
|
||||||
|
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserRoles(user: User): Role[] {
|
||||||
|
let roles = this._acls.value.find(
|
||||||
|
userRole => userRole.user_id === user.id
|
||||||
|
)
|
||||||
|
if (!roles) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return roles.role_ids.map(
|
||||||
|
role_id => this._roles.value.find(
|
||||||
|
role => role.id == role_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addUserRole(user: User, role: Role) {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: addUserRoleMutation,
|
||||||
|
variables: {
|
||||||
|
user_id: user.id,
|
||||||
|
role_id: role.id
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => {
|
||||||
|
this.getACLs().subscribe(
|
||||||
|
res => this._acls.next(res)
|
||||||
|
)
|
||||||
|
return res['data']['addUserRole']
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUserRole(user: User, role: Role) {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: deleteUserRoleMutation,
|
||||||
|
variables: {
|
||||||
|
user_id: user.id,
|
||||||
|
role_id: role.id
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => {
|
||||||
|
this.getACLs().subscribe(
|
||||||
|
res => this._acls.next(res)
|
||||||
|
)
|
||||||
|
return res['data']['deleteUserRole']
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUser(user: User) {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: deleteUserMutation,
|
||||||
|
variables: {
|
||||||
|
id: user.id,
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['deleteUser']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRole(role: Role) {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: deleteRoleMutation,
|
||||||
|
variables: {
|
||||||
|
id: role.id,
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['deleteRole']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
saveRole(role: Role) {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: saveRoleMutation,
|
||||||
|
variables: {
|
||||||
|
id: role.id,
|
||||||
|
name: role.name,
|
||||||
|
description: role.description
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['saveRole']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
saveUser(user: User) {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: saveUserMutation,
|
||||||
|
variables: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
active: user.active,
|
||||||
|
password: user.password,
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['saveUser']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
openUserDialog(user?: User) {
|
||||||
|
const dialogRef = this.dialog.open(GisafAdminAccessUserDialogComponent, {
|
||||||
|
width: '75%',
|
||||||
|
data: {
|
||||||
|
'user': user
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dialogRef.afterClosed().subscribe(
|
||||||
|
(user: User) => user &&
|
||||||
|
this.saveUser(
|
||||||
|
user
|
||||||
|
).subscribe({
|
||||||
|
next: (name: string) => {
|
||||||
|
if (user) {
|
||||||
|
// Update
|
||||||
|
//let row = this.dataSource.data.findIndex(c=>c['name'] == name)
|
||||||
|
//this.dataSource.data[row] = user
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// New
|
||||||
|
this.saveUser(user)
|
||||||
|
}
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
// Popup the dialog again, the error is catched by Apollo
|
||||||
|
this.openUserDialog(user)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
openRoleDialog(role?: Role) {
|
||||||
|
const dialogRef = this.dialog.open(GisafAdminAccessRoleDialogComponent, {
|
||||||
|
width: '75%',
|
||||||
|
data: {
|
||||||
|
'role': role
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dialogRef.afterClosed().subscribe(
|
||||||
|
(role: Role) => role &&
|
||||||
|
this.saveRole(
|
||||||
|
role
|
||||||
|
).subscribe({
|
||||||
|
next: (name: string) => {
|
||||||
|
if (role) {
|
||||||
|
// Update
|
||||||
|
//let row = this.dataSource.data.findIndex(c=>c['name'] == name)
|
||||||
|
//this.dataSource.data[row] = role
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// New
|
||||||
|
this.saveRole(role)
|
||||||
|
}
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
// Popup the dialog again, the error is catched by Apollo
|
||||||
|
this.openRoleDialog(role)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
31
src/app/admin/admin-manage/access/models.ts
Normal file
31
src/app/admin/admin-manage/access/models.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Observable, BehaviorSubject } from 'rxjs'
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
_roles: BehaviorSubject<Role[]>
|
||||||
|
roles$: Observable<Role[]>
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public name: string,
|
||||||
|
public email: string,
|
||||||
|
public active: boolean,
|
||||||
|
public password?: string,
|
||||||
|
) {
|
||||||
|
this._roles = new BehaviorSubject<Role[]>([])
|
||||||
|
this.roles$ = this._roles.asObservable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Role {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public name: string,
|
||||||
|
public description: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ACL {
|
||||||
|
constructor(
|
||||||
|
public user_id: number,
|
||||||
|
public role_ids: number[],
|
||||||
|
) {}
|
||||||
|
}
|
32
src/app/admin/admin-manage/access/role-dialog.component.html
Normal file
32
src/app/admin/admin-manage/access/role-dialog.component.html
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<h1 mat-dialog-title>Edit role</h1>
|
||||||
|
|
||||||
|
<div [formGroup]="formGroup" class='form'>
|
||||||
|
<div mat-dialog-content fxLayout="column">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>id</mat-label>
|
||||||
|
<input matInput
|
||||||
|
formControlName="id"
|
||||||
|
class="id"
|
||||||
|
[readonly]='true'
|
||||||
|
>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Name</mat-label>
|
||||||
|
<input matInput
|
||||||
|
formControlName="name"
|
||||||
|
class="name"
|
||||||
|
>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Description</mat-label>
|
||||||
|
<input matInput
|
||||||
|
formControlName="description"
|
||||||
|
class="description"
|
||||||
|
>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="dialogRef.close()">Cancel</button>
|
||||||
|
<button mat-button (click)="save()" type="submit" [disabled]="!formGroup.valid">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
34
src/app/admin/admin-manage/access/role-dialog.component.ts
Normal file
34
src/app/admin/admin-manage/access/role-dialog.component.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { Component, OnInit, Inject } from '@angular/core'
|
||||||
|
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'
|
||||||
|
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'
|
||||||
|
|
||||||
|
import { Role } from './models'
|
||||||
|
|
||||||
|
export interface DialogData {
|
||||||
|
role: Role
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-access-role-dialog',
|
||||||
|
templateUrl: 'role-dialog.component.html',
|
||||||
|
styleUrls: ['role-dialog.component.css'],
|
||||||
|
})
|
||||||
|
export class GisafAdminAccessRoleDialogComponent implements OnInit {
|
||||||
|
formGroup: UntypedFormGroup = new UntypedFormGroup({})
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<GisafAdminAccessRoleDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: DialogData
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
let role: Role = this.data['role'] || new Role(undefined, '', '')
|
||||||
|
this.formGroup.addControl('id', new UntypedFormControl(role.id))
|
||||||
|
this.formGroup.addControl('name', new UntypedFormControl(role.name, [Validators.required]))
|
||||||
|
this.formGroup.addControl('description', new UntypedFormControl(role.description, [Validators.required]))
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.dialogRef.close(this.formGroup.value);
|
||||||
|
}
|
||||||
|
}
|
26
src/app/admin/admin-manage/access/role.component.css
Normal file
26
src/app/admin/admin-manage/access/role.component.css
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
.container {
|
||||||
|
border: 1px solid grey;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 2px;
|
||||||
|
background-color: #424242;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 1px;
|
||||||
|
font-size: 110%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleBar .mat-mdc-button {
|
||||||
|
line-height: inherit;
|
||||||
|
min-width: inherit ! important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleBar .mat-mdc-button .mat-icon{
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
vertical-align: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
padding: 2px 3px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
13
src/app/admin/admin-manage/access/role.component.html
Normal file
13
src/app/admin/admin-manage/access/role.component.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<div class='container' cdkDrag [cdkDragData]="{'role': role}">
|
||||||
|
<div class='titleBar' fxLayout>
|
||||||
|
<div class='name' [title]='role.description' fxFlex>
|
||||||
|
{{ role.name }}
|
||||||
|
</div>
|
||||||
|
<button mat-button fxFlex='0 1 0%' matTooltip='Edit role' (click)='edit()'>
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-button fxFlex='0 1 0%' matTooltip='Delete role' (click)='delete()'>
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
31
src/app/admin/admin-manage/access/role.component.ts
Normal file
31
src/app/admin/admin-manage/access/role.component.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Component, ChangeDetectorRef, ChangeDetectionStrategy, Input } from '@angular/core'
|
||||||
|
|
||||||
|
import { Role } from './models'
|
||||||
|
import { AdminManageAccessDataService } from './access-data.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-role',
|
||||||
|
templateUrl: './role.component.html',
|
||||||
|
styleUrls: ['./role.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AdminRoleComponent {
|
||||||
|
@Input() role: Role
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
public adminManageAccessDataService: AdminManageAccessDataService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
this.adminManageAccessDataService.deleteRole(this.role).subscribe(
|
||||||
|
res => {
|
||||||
|
this.adminManageAccessDataService.init()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
edit() {
|
||||||
|
this.adminManageAccessDataService.openRoleDialog(this.role)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
.mat-mdc-dialog-content {
|
||||||
|
height: 20em;
|
||||||
|
}
|
47
src/app/admin/admin-manage/access/user-dialog.component.html
Normal file
47
src/app/admin/admin-manage/access/user-dialog.component.html
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<h1 mat-dialog-title>Edit user</h1>
|
||||||
|
|
||||||
|
<div [formGroup]="formGroup" class='form'>
|
||||||
|
<div mat-dialog-content fxLayout="column">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>id</mat-label>
|
||||||
|
<input matInput
|
||||||
|
readonly=true
|
||||||
|
formControlName="id"
|
||||||
|
class="id"
|
||||||
|
>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Name</mat-label>
|
||||||
|
<input matInput
|
||||||
|
formControlName="name"
|
||||||
|
class="name"
|
||||||
|
>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Password</mat-label>
|
||||||
|
<input matInput
|
||||||
|
type="password"
|
||||||
|
formControlName="password"
|
||||||
|
class="password"
|
||||||
|
>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Email</mat-label>
|
||||||
|
<input matInput
|
||||||
|
formControlName="email"
|
||||||
|
class="email"
|
||||||
|
>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-checkbox matInput
|
||||||
|
formControlName="active"
|
||||||
|
class="active"
|
||||||
|
>
|
||||||
|
Active
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="dialogRef.close()">Cancel</button>
|
||||||
|
<button mat-button (click)="save()" type="submit" [disabled]="!formGroup.valid">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
37
src/app/admin/admin-manage/access/user-dialog.component.ts
Normal file
37
src/app/admin/admin-manage/access/user-dialog.component.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Component, OnInit, Inject } from '@angular/core'
|
||||||
|
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'
|
||||||
|
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'
|
||||||
|
|
||||||
|
import { User } from './models'
|
||||||
|
|
||||||
|
export interface DialogData {
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-access-user-dialog',
|
||||||
|
templateUrl: 'user-dialog.component.html',
|
||||||
|
styleUrls: ['user-dialog.component.css'],
|
||||||
|
})
|
||||||
|
export class GisafAdminAccessUserDialogComponent implements OnInit {
|
||||||
|
formGroup: UntypedFormGroup
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<GisafAdminAccessUserDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: DialogData
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
let user: User = this.data['user'] || new User(undefined, '', '', true)
|
||||||
|
this.formGroup = new UntypedFormGroup({})
|
||||||
|
this.formGroup.addControl('id', new UntypedFormControl(user.id))
|
||||||
|
this.formGroup.addControl('name', new UntypedFormControl(user.name, [Validators.required]))
|
||||||
|
this.formGroup.addControl('email', new UntypedFormControl(user.email, [Validators.required, Validators.email]))
|
||||||
|
this.formGroup.addControl('password', new UntypedFormControl(''))
|
||||||
|
this.formGroup.addControl('active', new UntypedFormControl(user.active))
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.dialogRef.close(this.formGroup.value);
|
||||||
|
}
|
||||||
|
}
|
49
src/app/admin/admin-manage/access/user.component.css
Normal file
49
src/app/admin/admin-manage/access/user.component.css
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
.container {
|
||||||
|
border: 1px solid grey;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 1px;
|
||||||
|
background-color: #424242;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive .name {
|
||||||
|
color: #ffc0c0;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleBar .mat-mdc-button {
|
||||||
|
line-height: inherit;
|
||||||
|
min-width: inherit ! important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleBar .mat-mdc-button .mat-icon{
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
vertical-align: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: 600;
|
||||||
|
padding-left: 2px;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
font-size: 115%;
|
||||||
|
text-align: center;
|
||||||
|
color: #aaccbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drop-list {
|
||||||
|
min-height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-standard-chip.mat-mdcc-chip-with-trailing-icon {
|
||||||
|
min-height: 1.5em;
|
||||||
|
padding-right: 3px;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
26
src/app/admin/admin-manage/access/user.component.html
Normal file
26
src/app/admin/admin-manage/access/user.component.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<div class='container' [class]="user.active?'active':'inactive'"
|
||||||
|
cdkDropList
|
||||||
|
(cdkDropListDropped)="drop($event)"
|
||||||
|
>
|
||||||
|
<div class='titleBar' fxLayout>
|
||||||
|
<div class='name' [title]='user.email' fxFlex>
|
||||||
|
{{ user.name }}
|
||||||
|
</div>
|
||||||
|
<button mat-button fxFlex='0 1 0%' matTooltip='Edit user' (click)='edit()'>
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-button fxFlex='0 1 0%' matTooltip='Delete user' (click)='delete()'>
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<mat-chip-listbox #chipList>
|
||||||
|
<mat-chip-option *ngFor='let role of user.roles$ | async'
|
||||||
|
[selectable]='true'
|
||||||
|
[removable]='true'
|
||||||
|
(removed)="removeUserRole(user, role)"
|
||||||
|
>
|
||||||
|
{{ role.name }}
|
||||||
|
<mat-icon matChipRemove>cancel</mat-icon>
|
||||||
|
</mat-chip-option>
|
||||||
|
</mat-chip-listbox>
|
||||||
|
</div>
|
40
src/app/admin/admin-manage/access/user.component.ts
Normal file
40
src/app/admin/admin-manage/access/user.component.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { Component, ChangeDetectorRef, ChangeDetectionStrategy, Input } from '@angular/core'
|
||||||
|
import {COMMA, ENTER} from '@angular/cdk/keycodes'
|
||||||
|
|
||||||
|
import { User, Role } from './models'
|
||||||
|
import { AdminManageAccessDataService } from './access-data.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-user',
|
||||||
|
templateUrl: './user.component.html',
|
||||||
|
styleUrls: ['./user.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AdminUserComponent {
|
||||||
|
@Input() user: User
|
||||||
|
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
public adminManageAccessDataService: AdminManageAccessDataService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
drop(evt) {
|
||||||
|
// Assign a role to the user
|
||||||
|
this.adminManageAccessDataService.addUserRole(this.user, evt.item.data['role']).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUserRole(user: User, role: Role) {
|
||||||
|
this.adminManageAccessDataService.deleteUserRole(user, role).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
this.adminManageAccessDataService.deleteUser(this.user).subscribe(
|
||||||
|
res => this.adminManageAccessDataService.init()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
edit() {
|
||||||
|
this.adminManageAccessDataService.openUserDialog(this.user)
|
||||||
|
}
|
||||||
|
}
|
24
src/app/admin/admin-manage/access/users-roles.component.css
Normal file
24
src/app/admin/admin-manage/access/users-roles.component.css
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteItem {
|
||||||
|
height: 2em;
|
||||||
|
border: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users {
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
gisaf-admin-user {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roles {
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
gisaf-admin-role {
|
||||||
|
padding: 0;
|
||||||
|
}
|
30
src/app/admin/admin-manage/access/users-roles.component.html
Normal file
30
src/app/admin/admin-manage/access/users-roles.component.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<div cdkDropListGroup fxFlexFill>
|
||||||
|
<h1>Users and roles</h1>
|
||||||
|
<div fxLayout='row'>
|
||||||
|
<div class='users' fxFlex='3 1 0'>
|
||||||
|
<div>Users</div>
|
||||||
|
<button mat-raised-button (click)='adminManageAccessDataService.openUserDialog()'>New user...</button>
|
||||||
|
<div id='gisaf-admin-users'>
|
||||||
|
<gisaf-admin-user
|
||||||
|
*ngFor="let user of adminManageAccessDataService.users$ | async"
|
||||||
|
[user]="user"
|
||||||
|
>
|
||||||
|
</gisaf-admin-user>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='roles' fxFlex>
|
||||||
|
<div>Roles (<span style='font-style: italic;'>Drag to users</span>)
|
||||||
|
</div>
|
||||||
|
<button mat-raised-button (click)='adminManageAccessDataService.openRoleDialog()'>New role...</button>
|
||||||
|
|
||||||
|
<div cdkDropList cdkDropListSortingDisabled id='gisaf-admin-roles'>
|
||||||
|
<gisaf-admin-role
|
||||||
|
*ngFor="let role of adminManageAccessDataService.roles$ | async"
|
||||||
|
[role]="role"
|
||||||
|
>
|
||||||
|
</gisaf-admin-role>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
22
src/app/admin/admin-manage/access/users-roles.component.ts
Normal file
22
src/app/admin/admin-manage/access/users-roles.component.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { Component, OnInit, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
|
||||||
|
import { ActivatedRoute } from '@angular/router'
|
||||||
|
|
||||||
|
import { AdminManageAccessDataService } from './access-data.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-users-roles',
|
||||||
|
templateUrl: './users-roles.component.html',
|
||||||
|
styleUrls: ['./users-roles.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AdminAccessComponent implements OnInit {
|
||||||
|
constructor(
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
public adminManageAccessDataService: AdminManageAccessDataService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.adminManageAccessDataService.init()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { AdminManageDataService, Category } from '../data.service'
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CategoryResolver implements Resolve<object> {
|
||||||
|
constructor(
|
||||||
|
private adminManageDataService: AdminManageDataService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Category[]> {
|
||||||
|
return this.adminManageDataService.getCategories()
|
||||||
|
}
|
||||||
|
}
|
51
src/app/admin/admin-manage/category/category.component.css
Normal file
51
src/app/admin/admin-manage/category/category.component.css
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools button {
|
||||||
|
width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter button {
|
||||||
|
min-width: 1.5em!important;
|
||||||
|
max-width: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.cdk-column-symbol {
|
||||||
|
font-family: "GisafSymbols";
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.even {
|
||||||
|
background-color: #f5f5f512;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unlockCheckbox {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
td .mat-mdc-button {
|
||||||
|
min-width: inherit ! important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-column-delete, .mat-column-edit {
|
||||||
|
width: 3em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-column-symbol {
|
||||||
|
width: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.mat-mdc-header-cell:first-of-type, td.mat-mdc-cell:first-of-type, td.mat-mdc-footer-cell:first-of-type {
|
||||||
|
padding-left: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.mat-mdc-header-cell:last-of-type, td.mat-mdc-cell:last-of-type, td.mat-mdc-footer-cell:last-of-type {
|
||||||
|
padding-right: inherit;
|
||||||
|
}
|
82
src/app/admin/admin-manage/category/category.component.html
Normal file
82
src/app/admin/admin-manage/category/category.component.html
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<h1>Survey categories</h1>
|
||||||
|
<div class='tools' fxLayout='row' fxLayoutAlign="space-around center">
|
||||||
|
<mat-form-field class='filter' fxFlex="1 1 0">
|
||||||
|
<mat-label>Filter in table</mat-label>
|
||||||
|
<input matInput
|
||||||
|
(input)="applyFilter()"
|
||||||
|
matTooltip="Filter the items from the table"
|
||||||
|
[(ngModel)]="filterText"/>
|
||||||
|
<button mat-button matSuffix mat-icon-button
|
||||||
|
*ngIf="filterText"
|
||||||
|
aria-label="Clear"
|
||||||
|
(click)="filterText='';applyFilter()"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
<button mat-raised-button aria-label="New"
|
||||||
|
*ngIf="authenticationService.isAuthorized(edit_roles)"
|
||||||
|
(click)="editItem()">
|
||||||
|
<mat-icon>create</mat-icon>
|
||||||
|
Add new category
|
||||||
|
</button>
|
||||||
|
<!--
|
||||||
|
<button mat-raised-button aria-label="Update Gisaf registry"
|
||||||
|
matTooltip='After change in categories, Gisaf needs to update database tables and update its registry of categories'
|
||||||
|
(click)="updateRegistry()">
|
||||||
|
<mat-icon>update</mat-icon>
|
||||||
|
Update Gisaf registry
|
||||||
|
</button>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
<table mat-table matSort [dataSource]="dataSource" class='content'
|
||||||
|
matSortActive="name" matSortDirection="asc">
|
||||||
|
<ng-container [matColumnDef]="field" *ngFor='let field of fields'>
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ field }}</th>
|
||||||
|
<td mat-cell *matCellDef="let item">{{ item[field] }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="delete"
|
||||||
|
*ngIf="authenticationService.isAuthorized(edit_roles)"
|
||||||
|
>
|
||||||
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
|
<form [formGroup]="unlockDeleteFormGroup" class='unlockCheckbox'>
|
||||||
|
<mat-checkbox formControlName='canDelete' matTooltip='Unlock delete buttons'>
|
||||||
|
</mat-checkbox>
|
||||||
|
</form>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let item">
|
||||||
|
<button mat-button (click)="deleteItem(item)"
|
||||||
|
[disabled]="!unlockDeleteFormGroup.controls['canDelete'].value"
|
||||||
|
matTooltip="Delete"
|
||||||
|
>
|
||||||
|
<mat-icon aria-label="delete">delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="edit">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Edit</th>
|
||||||
|
<td mat-cell *matCellDef="let item">
|
||||||
|
<button mat-button (click)="editItem(item)"
|
||||||
|
matTooltip="Edit the item"
|
||||||
|
>
|
||||||
|
<mat-icon aria-label="edit">edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="columns;sticky:true"></tr>
|
||||||
|
<tr mat-row [ngClass]="{even: even}"
|
||||||
|
*matRowDef="let row; let even = even;columns:columns"
|
||||||
|
></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<mat-paginator
|
||||||
|
class="paginator"
|
||||||
|
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||||
|
[pageIndex]='0'
|
||||||
|
[pageSize]='10'
|
||||||
|
showFirstLastButtons
|
||||||
|
>
|
||||||
|
</mat-paginator>
|
136
src/app/admin/admin-manage/category/category.component.ts
Normal file
136
src/app/admin/admin-manage/category/category.component.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import { Component, OnInit,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy, ViewChild } from '@angular/core'
|
||||||
|
import { ActivatedRoute } from '@angular/router'
|
||||||
|
import { UntypedFormGroup, UntypedFormControl } from '@angular/forms'
|
||||||
|
|
||||||
|
import { MatTableDataSource } from '@angular/material/table'
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar'
|
||||||
|
import { MatSort } from '@angular/material/sort'
|
||||||
|
import { MatPaginator } from '@angular/material/paginator'
|
||||||
|
import { MatDialog } from '@angular/material/dialog'
|
||||||
|
|
||||||
|
import { GisafAdminCategoryDialogComponent } from './dialog.component'
|
||||||
|
import { Category, AdminManageDataService } from '../data.service'
|
||||||
|
import { AuthenticationService } from '../../../_services/authentication.service'
|
||||||
|
import { CategoryField, categoryFields } from './fields'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-category',
|
||||||
|
templateUrl: './category.component.html',
|
||||||
|
styleUrls: ['./category.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class CategoryComponent implements OnInit {
|
||||||
|
edit_roles = ['category manager']
|
||||||
|
categories: Category[] = []
|
||||||
|
fields: string[] = categoryFields.map(f=>f.name)
|
||||||
|
actions: string[] = [
|
||||||
|
'edit',
|
||||||
|
'delete'
|
||||||
|
]
|
||||||
|
columns: string[]
|
||||||
|
dataSource: MatTableDataSource<object>
|
||||||
|
@ViewChild(MatSort, {static: true}) sort: MatSort
|
||||||
|
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator
|
||||||
|
unlockDeleteFormGroup: UntypedFormGroup = new UntypedFormGroup({})
|
||||||
|
filterText: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
|
private adminManageDataService: AdminManageDataService,
|
||||||
|
public authenticationService: AuthenticationService,
|
||||||
|
public dialog: MatDialog,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.route.data.subscribe(
|
||||||
|
data => {
|
||||||
|
this.categories = data['categories']
|
||||||
|
this.dataSource = new MatTableDataSource(this.categories)
|
||||||
|
this.dataSource.sort = this.sort
|
||||||
|
this.dataSource.paginator = this.paginator
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.unlockDeleteFormGroup = new UntypedFormGroup({
|
||||||
|
'canDelete': new UntypedFormControl(),
|
||||||
|
})
|
||||||
|
this.columns = categoryFields.filter(f=>f.inTable).map(f=>f.name)
|
||||||
|
if (this.authenticationService.isAuthorized(this.edit_roles)) {
|
||||||
|
this.columns = this.columns.concat(this.actions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter() {
|
||||||
|
this.dataSource.filter = this.filterText.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
editItem(data?: Category) {
|
||||||
|
const dialogRef = this.dialog.open(GisafAdminCategoryDialogComponent, {
|
||||||
|
width: '75%',
|
||||||
|
data: {
|
||||||
|
'category': data || new Category(''),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dialogRef.afterClosed().subscribe(
|
||||||
|
(category: Category) => category &&
|
||||||
|
this.adminManageDataService.saveCategory(
|
||||||
|
category
|
||||||
|
).subscribe({
|
||||||
|
next: (result: object) => {
|
||||||
|
let _mode: string = result['_mode']
|
||||||
|
if (_mode == 'updated') {
|
||||||
|
let row = this.dataSource.data.findIndex(c=>c['name'] == category.name)
|
||||||
|
this.dataSource.data[row] = category
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.dataSource.data.push(category as Category)
|
||||||
|
}
|
||||||
|
let msg = 'Category ' + category.name + ' ' + result['_mode']
|
||||||
|
if (result['message']) {
|
||||||
|
msg += result['message']
|
||||||
|
}
|
||||||
|
this.snackBar.open(msg, 'Close')
|
||||||
|
this.dataSource.data = this.dataSource.data
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
// Popup the dialog again, the error is catched by Apollo
|
||||||
|
console.error(err)
|
||||||
|
this.editItem(category)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteItem(category: Category) {
|
||||||
|
this.adminManageDataService.deleteCategory(category).subscribe({
|
||||||
|
next: (message: string) => {
|
||||||
|
this.snackBar.open(message, 'Close')
|
||||||
|
// Remove from table
|
||||||
|
let dsi = this.dataSource.data.findIndex(fi => fi['name'] == category.name)
|
||||||
|
this.dataSource.data.splice(dsi, 1)
|
||||||
|
// Force Angular change detection (??)
|
||||||
|
this.dataSource.data = this.dataSource.data
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
this.snackBar.open(err, 'Close')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRegistry() {
|
||||||
|
this.adminManageDataService.updateRegistry().subscribe(
|
||||||
|
resp => {
|
||||||
|
this.snackBar.open(
|
||||||
|
'Success: tables created, registry updated, other Gisaf processes notified',
|
||||||
|
'Close',
|
||||||
|
{duration: 3000}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
8
src/app/admin/admin-manage/category/dialog.component.css
Normal file
8
src/app/admin/admin-manage/category/dialog.component.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
mat-form-field {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
font-family: "GisafSymbols";
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
17
src/app/admin/admin-manage/category/dialog.component.html
Normal file
17
src/app/admin/admin-manage/category/dialog.component.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<h1 mat-dialog-title>Edit category</h1>
|
||||||
|
|
||||||
|
<div [formGroup]="formGroup" class='form'>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<mat-form-field *ngFor="let keyvalue of formGroup.controls | keyvalue">
|
||||||
|
<mat-label>{{ keyvalue.key }}</mat-label>
|
||||||
|
<input matInput
|
||||||
|
[formControlName]="keyvalue.key"
|
||||||
|
[class]="keyvalue.key"
|
||||||
|
>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="dialogRef.close()">Cancel</button>
|
||||||
|
<button mat-button (click)="save()" type="submit" [disabled]="!formGroup.valid">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
46
src/app/admin/admin-manage/category/dialog.component.ts
Normal file
46
src/app/admin/admin-manage/category/dialog.component.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { Component, OnInit, Input,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy, ViewChild, Inject } from '@angular/core'
|
||||||
|
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'
|
||||||
|
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'
|
||||||
|
|
||||||
|
import { Category } from '../data.service'
|
||||||
|
import { categoryFields } from './fields'
|
||||||
|
|
||||||
|
export interface DialogData {
|
||||||
|
category: Category
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-category-dialog',
|
||||||
|
templateUrl: 'dialog.component.html',
|
||||||
|
styleUrls: ['dialog.component.css'],
|
||||||
|
})
|
||||||
|
export class GisafAdminCategoryDialogComponent implements OnInit {
|
||||||
|
formGroup: UntypedFormGroup
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<GisafAdminCategoryDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: DialogData
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
let category: Category = this.data['category']
|
||||||
|
this.formGroup = new UntypedFormGroup({})
|
||||||
|
//this.formGroup.addControl('name', new FormControl(category.name, [Validators.required]))
|
||||||
|
for (let categoryField of categoryFields) {
|
||||||
|
this.formGroup.addControl(
|
||||||
|
categoryField.name,
|
||||||
|
new UntypedFormControl(
|
||||||
|
category[categoryField.name] || categoryField.defaultValue,
|
||||||
|
categoryField.validators,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.dialogRef.close(this.formGroup.value);
|
||||||
|
}
|
||||||
|
}
|
29
src/app/admin/admin-manage/category/fields.ts
Normal file
29
src/app/admin/admin-manage/category/fields.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Validators } from '@angular/forms'
|
||||||
|
|
||||||
|
export class CategoryField {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public validators = [],
|
||||||
|
public type: string = 'string',
|
||||||
|
public defaultValue?: string,
|
||||||
|
public inTable: boolean = true
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const categoryFields: CategoryField[] = [
|
||||||
|
new CategoryField('name', [Validators.required]),
|
||||||
|
new CategoryField('description', [Validators.required]),
|
||||||
|
new CategoryField('group', [Validators.required, Validators.maxLength(4), Validators.minLength(4)]),
|
||||||
|
new CategoryField('minor_group_1', [Validators.required, Validators.maxLength(4), Validators.minLength(4)]),
|
||||||
|
new CategoryField('minor_group_2', [Validators.required, Validators.maxLength(4), Validators.minLength(4)], 'string', '----'),
|
||||||
|
new CategoryField('status', [Validators.required, Validators.maxLength(1), Validators.minLength(1)], 'string', 'E'),
|
||||||
|
//new CategoryField('auto_import', [], 'boolean'),
|
||||||
|
new CategoryField('model_type', [Validators.required, Validators.pattern('^(Point|Line|Polygon)$')], 'string', 'Point'),
|
||||||
|
new CategoryField('long_name'),
|
||||||
|
new CategoryField('symbol', [Validators.maxLength(1), Validators.minLength(1)], 'string'),
|
||||||
|
new CategoryField('mapbox_type_custom', [], 'string', '', false),
|
||||||
|
new CategoryField('mapbox_paint', [], 'string', '', false),
|
||||||
|
new CategoryField('mapbox_layout', [], 'string', '', false),
|
||||||
|
new CategoryField('viewable_role', [], 'string', '', false),
|
||||||
|
new CategoryField('extra', [], 'string', '', false),
|
||||||
|
]
|
525
src/app/admin/admin-manage/data.service.ts
Normal file
525
src/app/admin/admin-manage/data.service.ts
Normal file
|
@ -0,0 +1,525 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
|
||||||
|
import { Observable, BehaviorSubject, forkJoin, of as observableOf } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { Apollo, gql } from 'apollo-angular'
|
||||||
|
|
||||||
|
const getSurveyStores = gql`
|
||||||
|
query geo_survey_stores {
|
||||||
|
geo_survey_stores {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const getRawSurveyStores = gql`
|
||||||
|
query geo_raw_survey_stores {
|
||||||
|
geo_raw_survey_stores {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const getRawSurveyPoints = gql`
|
||||||
|
query raw_survey_points ($orig_id: String!) {
|
||||||
|
raw_survey_points(orig_id: $orig_id) {
|
||||||
|
id
|
||||||
|
category
|
||||||
|
date
|
||||||
|
orig_id
|
||||||
|
store
|
||||||
|
status
|
||||||
|
type
|
||||||
|
project
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const reconciliationItemMutation = gql`
|
||||||
|
mutation reconciliationItem(
|
||||||
|
$id: String!,
|
||||||
|
$category: String!
|
||||||
|
) {
|
||||||
|
reconciliationItem(
|
||||||
|
id: $id,
|
||||||
|
category: $category
|
||||||
|
) {
|
||||||
|
result {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const changeSurveyPointMutation = gql`
|
||||||
|
mutation changeSurveyPointStatus(
|
||||||
|
$id: String!,
|
||||||
|
$status: String!
|
||||||
|
) {
|
||||||
|
changePointStatus(
|
||||||
|
id: $id,
|
||||||
|
status: $status
|
||||||
|
) {
|
||||||
|
result {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const deleteSurveyPointMutation = gql`
|
||||||
|
mutation deletePoint(
|
||||||
|
$id: String!,
|
||||||
|
) {
|
||||||
|
deletePoint(
|
||||||
|
id: $id,
|
||||||
|
) {
|
||||||
|
result {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
const categoryMutation = gql`
|
||||||
|
mutation categoryMutation(
|
||||||
|
$category: CategoryInput!,
|
||||||
|
) {
|
||||||
|
editCategory(
|
||||||
|
category: $category,
|
||||||
|
) {
|
||||||
|
result {
|
||||||
|
name
|
||||||
|
_message
|
||||||
|
_mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const projectMutation = gql`
|
||||||
|
mutation projectMutation(
|
||||||
|
$project: ProjectInput!,
|
||||||
|
) {
|
||||||
|
editProject(
|
||||||
|
project: $project,
|
||||||
|
) {
|
||||||
|
result {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const categoryDeleteMutation = gql`
|
||||||
|
mutation deleteCategory(
|
||||||
|
$name: String!
|
||||||
|
) {
|
||||||
|
deleteCategory(
|
||||||
|
name: $name,
|
||||||
|
) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const projectDeleteMutation = gql`
|
||||||
|
mutation deleteProject(
|
||||||
|
$id: Int!
|
||||||
|
) {
|
||||||
|
deleteProject(
|
||||||
|
id: $id,
|
||||||
|
) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const categoriesQuery = gql`
|
||||||
|
query categories {
|
||||||
|
categories {
|
||||||
|
name
|
||||||
|
description
|
||||||
|
group
|
||||||
|
minor_group_1
|
||||||
|
minor_group_2
|
||||||
|
status
|
||||||
|
auto_import
|
||||||
|
model_type
|
||||||
|
long_name
|
||||||
|
symbol
|
||||||
|
mapbox_type_custom
|
||||||
|
mapbox_paint
|
||||||
|
mapbox_layout
|
||||||
|
viewable_role
|
||||||
|
extra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const projectsQuery = gql`
|
||||||
|
query projects {
|
||||||
|
projects {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
contact_person
|
||||||
|
site
|
||||||
|
date_approved
|
||||||
|
start_date_planned
|
||||||
|
start_date_effective
|
||||||
|
end_date_planned
|
||||||
|
end_date_effective
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const updateRegistryQuery = gql`
|
||||||
|
query updateRegistry {
|
||||||
|
updateRegistry
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const vacuumDbQuery = gql`
|
||||||
|
query vacuumDb {
|
||||||
|
vacuumDb
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const runIntegrityCheckQuery = gql`
|
||||||
|
query runIntegrityCheck(
|
||||||
|
$integrityCheckId: String!
|
||||||
|
) {
|
||||||
|
integrityCheckRun(
|
||||||
|
integrityCheckId: $integrityCheckId
|
||||||
|
) {
|
||||||
|
name
|
||||||
|
description
|
||||||
|
dfData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const integrityCheckQuery = gql`
|
||||||
|
query integrityChecks {
|
||||||
|
integrityChecks {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export class Store {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Category {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public description?: string,
|
||||||
|
public group?: string,
|
||||||
|
public minor_group_1?: string,
|
||||||
|
public minor_group_2?: string,
|
||||||
|
public status?: string,
|
||||||
|
public auto_import?: string,
|
||||||
|
public model_type?: string,
|
||||||
|
public long_name?: string,
|
||||||
|
public symbol?: string,
|
||||||
|
public mapbox_type_custom?: string,
|
||||||
|
public mapbox_paint?: string,
|
||||||
|
public mapbox_layout?: string,
|
||||||
|
public viewable_role?: string,
|
||||||
|
public extra?: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Project {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public name: string,
|
||||||
|
public contact_person?: string,
|
||||||
|
public site?: string,
|
||||||
|
public date_approved?: Date,
|
||||||
|
public start_date_planned?: Date,
|
||||||
|
public start_date_effective?: Date,
|
||||||
|
public end_date_planned?: Date,
|
||||||
|
public end_date_effective?: Date,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RawSurveyPoint {
|
||||||
|
constructor(
|
||||||
|
public id: String,
|
||||||
|
public orig_id: String,
|
||||||
|
public date: Date,
|
||||||
|
public category: String,
|
||||||
|
public status: String,
|
||||||
|
public store: String,
|
||||||
|
public type: String,
|
||||||
|
public project: String,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Field {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public type: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DF {
|
||||||
|
constructor(
|
||||||
|
public fields: Field[],
|
||||||
|
public rows: object[]
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get fieldNames() {
|
||||||
|
return this.fields.map(f=>f.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IntegrityReport {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public description: string,
|
||||||
|
public df?: DF,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IntegrityCheck {
|
||||||
|
constructor(
|
||||||
|
public id: string,
|
||||||
|
public name: string,
|
||||||
|
public description: string,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminManageDataService {
|
||||||
|
constructor(
|
||||||
|
private apollo: Apollo,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getRawSurveyStores(): Observable<Store[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getRawSurveyStores,
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['geo_raw_survey_stores'].map(
|
||||||
|
(store: object) => new Store(
|
||||||
|
store['name'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
getSurveyStores(): Observable<Store[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getSurveyStores,
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['geo_survey_stores'].map(
|
||||||
|
(store: object) => new Store(
|
||||||
|
store['name'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
getRawSurveyPoints(orig_id: string): Observable<RawSurveyPoint[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: getRawSurveyPoints,
|
||||||
|
variables: {
|
||||||
|
orig_id: orig_id
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['raw_survey_points'].map(
|
||||||
|
(rawSurveyPoint: object) => new RawSurveyPoint(
|
||||||
|
rawSurveyPoint['id'],
|
||||||
|
rawSurveyPoint['orig_id'],
|
||||||
|
rawSurveyPoint['date'],
|
||||||
|
rawSurveyPoint['category'],
|
||||||
|
rawSurveyPoint['status'],
|
||||||
|
rawSurveyPoint['store'],
|
||||||
|
rawSurveyPoint['type'],
|
||||||
|
rawSurveyPoint['project'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
reconcile(item: RawSurveyPoint, category: string): Observable<string> {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: reconciliationItemMutation,
|
||||||
|
variables: {
|
||||||
|
id: item.id,
|
||||||
|
category: category
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['reconciliationItem']['result']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
changeStatus(item: RawSurveyPoint, status: string): Observable<string> {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: changeSurveyPointMutation,
|
||||||
|
variables: {
|
||||||
|
id: item.id,
|
||||||
|
status: status
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['changePointStatus']['result']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(item: RawSurveyPoint): Observable<string> {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: deleteSurveyPointMutation,
|
||||||
|
variables: {
|
||||||
|
id: item.id,
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['deletePoint']['result']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategories(): Observable<Category[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: categoriesQuery,
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['categories'].map(
|
||||||
|
(category: object) => new Category(
|
||||||
|
category['name'],
|
||||||
|
category['description'],
|
||||||
|
category['group'],
|
||||||
|
category['minor_group_1'],
|
||||||
|
category['minor_group_2'],
|
||||||
|
category['status'],
|
||||||
|
category['auto_import'],
|
||||||
|
category['model_type'],
|
||||||
|
category['long_name'],
|
||||||
|
category['symbol'],
|
||||||
|
category['mapbox_type_custom'],
|
||||||
|
category['mapbox_paint'],
|
||||||
|
category['mapbox_layout'],
|
||||||
|
category['viewable_role'],
|
||||||
|
category['extra'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
getProjects(): Observable<Project[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: projectsQuery,
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['projects'].map(
|
||||||
|
(project: object) => project as Project
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCategory(category: Category): Observable<object> {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: categoryMutation,
|
||||||
|
variables: {
|
||||||
|
category: category,
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['editCategory']['result']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCategory(category: Category): Observable<string> {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: categoryDeleteMutation,
|
||||||
|
variables: {
|
||||||
|
name: category.name,
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
// TODO: distinguish message and error
|
||||||
|
res => res['data']['deleteCategory']['result']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
saveProject(project: Project): Observable<number> {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: projectMutation,
|
||||||
|
variables: {
|
||||||
|
project: project,
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['editProject']['result']['id']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteProject(project: Project): Observable<number> {
|
||||||
|
return this.apollo.mutate({
|
||||||
|
mutation: projectDeleteMutation,
|
||||||
|
variables: {
|
||||||
|
id: project.id,
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['deleteProject']['result']['id']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRegistry() {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: updateRegistryQuery,
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['updateRegistry']['result']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
vacuumDb(): Observable<boolean> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: vacuumDbQuery,
|
||||||
|
}).pipe(map(
|
||||||
|
res => res['data']['vacuumDb']
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
runIntegrityCheck(integrityCheck: IntegrityCheck): Observable<IntegrityReport> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: runIntegrityCheckQuery,
|
||||||
|
variables: {
|
||||||
|
integrityCheckId: integrityCheck.id
|
||||||
|
}
|
||||||
|
}).pipe(map(
|
||||||
|
res => {
|
||||||
|
let report = res['data']['integrityCheckRun']
|
||||||
|
let dfData = JSON.parse(report['dfData'])
|
||||||
|
return new IntegrityReport(
|
||||||
|
report['name'],
|
||||||
|
report['description'],
|
||||||
|
dfData && new DF(
|
||||||
|
dfData['schema']['fields'].map(
|
||||||
|
field => new Field(field['name'], field['type'])
|
||||||
|
),
|
||||||
|
dfData['data']
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
getIntegrityChecks(): Observable<IntegrityCheck[]> {
|
||||||
|
return this.apollo.query({
|
||||||
|
query: integrityCheckQuery
|
||||||
|
}).pipe(map(
|
||||||
|
resp => {
|
||||||
|
let data = resp['data']['integrityChecks']
|
||||||
|
return data.map(
|
||||||
|
(item: object) => new IntegrityCheck(
|
||||||
|
item['id'],
|
||||||
|
item['name'],
|
||||||
|
item['description']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
19
src/app/admin/admin-manage/integrity/integrity.component.css
Normal file
19
src/app/admin/admin-manage/integrity/integrity.component.css
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
.buttons {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button {
|
||||||
|
margin: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.mat-mdc-header-cell, td.mat-mdc-cell, td.mat-mdc-footer-cell {
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.even {
|
||||||
|
background-color: #f5f5f512;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
<div class='buttons'>
|
||||||
|
<span>Integrity checks:</span>
|
||||||
|
<button mat-raised-button
|
||||||
|
*ngFor='let integrityCheck of integrityChecks'
|
||||||
|
[title]='integrityCheck.description'
|
||||||
|
(click)='runIntegrityCheck(integrityCheck)'
|
||||||
|
>
|
||||||
|
<mat-icon>report</mat-icon>
|
||||||
|
{{ integrityCheck.name }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<mat-progress-bar
|
||||||
|
*ngIf='pleaseWait'
|
||||||
|
mode='indeterminate'
|
||||||
|
color='accent'
|
||||||
|
value=100
|
||||||
|
>
|
||||||
|
</mat-progress-bar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf='report'>
|
||||||
|
<h2>{{ report.name }}</h2>
|
||||||
|
<p>{{ report.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class='tools'>
|
||||||
|
<mat-form-field class='filter'>
|
||||||
|
<mat-label>Filter in report</mat-label>
|
||||||
|
<input matInput
|
||||||
|
(input)='applyFilter()'
|
||||||
|
matTooltip='Filter the items of the report from the table'
|
||||||
|
[(ngModel)]='filterText'/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table mat-table matSort [dataSource]='dataSource'>
|
||||||
|
<ng-container *ngFor='let colName of fieldNames'
|
||||||
|
[matColumnDef]='colName'>
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ colName }}</th>
|
||||||
|
<td mat-cell *matCellDef="let value"> {{ value[colName] }} </td>
|
||||||
|
</ng-container>
|
||||||
|
<tr mat-header-row *matHeaderRowDef="fieldNames; sticky: true"></tr>
|
||||||
|
<tr mat-row [ngClass]="{even: even}"
|
||||||
|
*matRowDef="let row; let even = even; columns: fieldNames"></tr>
|
||||||
|
</table>
|
||||||
|
<mat-paginator
|
||||||
|
class="paginator"
|
||||||
|
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||||
|
[pageIndex]='0'
|
||||||
|
[pageSize]='10'
|
||||||
|
showFirstLastButtons
|
||||||
|
>
|
||||||
|
</mat-paginator>
|
67
src/app/admin/admin-manage/integrity/integrity.component.ts
Normal file
67
src/app/admin/admin-manage/integrity/integrity.component.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { Component, OnInit, ViewChild,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
||||||
|
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar'
|
||||||
|
import { MatPaginator } from '@angular/material/paginator'
|
||||||
|
import { MatSort } from '@angular/material/sort'
|
||||||
|
import { MatTableDataSource } from '@angular/material/table'
|
||||||
|
|
||||||
|
import { AdminManageDataService, IntegrityReport, IntegrityCheck } from '../data.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-integrity',
|
||||||
|
templateUrl: './integrity.component.html',
|
||||||
|
styleUrls: ['./integrity.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AdminIntegrityComponent implements OnInit {
|
||||||
|
integrityChecks: IntegrityCheck[] = []
|
||||||
|
report: IntegrityReport
|
||||||
|
pleaseWait: boolean = false
|
||||||
|
filterText: string
|
||||||
|
dataSource: MatTableDataSource<object>
|
||||||
|
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator
|
||||||
|
@ViewChild(MatSort, {static: true}) sort: MatSort
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public adminManageDataService: AdminManageDataService,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.adminManageDataService.getIntegrityChecks().subscribe(
|
||||||
|
resp => {
|
||||||
|
this.integrityChecks = resp
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get fieldNames(): string[] {
|
||||||
|
return this.report && this.report.df ? this.report.df.fieldNames : []
|
||||||
|
}
|
||||||
|
|
||||||
|
runIntegrityCheck(integrityCheck: IntegrityCheck) {
|
||||||
|
this.pleaseWait = true
|
||||||
|
this.adminManageDataService.runIntegrityCheck(integrityCheck).subscribe(
|
||||||
|
(report: IntegrityReport) => {
|
||||||
|
this.pleaseWait = false
|
||||||
|
this.snackBar.open(
|
||||||
|
`Integrity check ${integrityCheck.name} executed`,
|
||||||
|
'Close',
|
||||||
|
{ duration: 3000 }
|
||||||
|
)
|
||||||
|
this.report = report
|
||||||
|
this.dataSource = new MatTableDataSource(report.df ? report.df.rows : [])
|
||||||
|
this.dataSource.sort = this.sort
|
||||||
|
this.dataSource.paginator = this.paginator
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter() {
|
||||||
|
this.dataSource.filter = this.filterText.trim().toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
<button mat-button (click)="vacuumDb()">
|
||||||
|
<mat-icon>healing</mat-icon>
|
||||||
|
Vacuum DB
|
||||||
|
</button>
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Component, OnInit, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
||||||
|
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar'
|
||||||
|
|
||||||
|
import { AdminManageDataService } from '../data.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-maintenance',
|
||||||
|
templateUrl: './maintenance.component.html',
|
||||||
|
styleUrls: ['./maintenance.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AdminMaintenanceComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private adminManageDataService: AdminManageDataService,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
vacuumDb() {
|
||||||
|
this.adminManageDataService.vacuumDb().subscribe(
|
||||||
|
resp => this.snackBar.open(
|
||||||
|
'Database vacuum OK',
|
||||||
|
'Close',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
.destination {
|
||||||
|
width: 40em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form, .points {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seachForm {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opForm {
|
||||||
|
border: 1px solid grey;
|
||||||
|
padding: 2px 1em;
|
||||||
|
border-radius: 0.6em;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
background-color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newStatus {
|
||||||
|
width: 6em;
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
<form>
|
||||||
|
<div [formGroup]="formGroup" class='form' fxLayout="row">
|
||||||
|
<div fxFlex fxLayout="column" class='seachForm'>
|
||||||
|
<mat-form-field fxFlex='0 0 0%'>
|
||||||
|
<mat-label>Original ID (search in the raw survey points)</mat-label>
|
||||||
|
<input matInput type="text"
|
||||||
|
formControlName="originalId"
|
||||||
|
required/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button type="submit" class="submitButton"
|
||||||
|
fxFlex='0 0 0%'
|
||||||
|
mat-raised-button
|
||||||
|
color='secondary'
|
||||||
|
(click)="submit()"
|
||||||
|
[disabled]="!formGroup.valid">
|
||||||
|
Search points
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div fxFlex fxLayout="column" class='opForm'>
|
||||||
|
<mat-form-field fxFlex class='newStatus'>
|
||||||
|
<mat-label>New status</mat-label>
|
||||||
|
<input matInput type="text" formControlName="newStatus"/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field class='destination'>
|
||||||
|
<mat-label>Reconciliation destination</mat-label>
|
||||||
|
<input matInput type="text" formControlName="destination"
|
||||||
|
[matAutocomplete]="auto"/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-autocomplete #auto="matAutocomplete">
|
||||||
|
<mat-option *ngFor="let category of filteredCategories | async" [value]="category.name">
|
||||||
|
{{ category.name }} ({{ category.group }}-{{ category.minor_group_1}}, {{ category.description }}, {{ category.status }})
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
|
||||||
|
<mat-checkbox formControlName="canDelete">
|
||||||
|
Enable delete buttons
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<hr/>
|
||||||
|
<div fxLayout="column" fxLayoutAlign="center start" class="points">
|
||||||
|
<gisaf-admin-raw-survey-point fxFlex *ngFor="let rawSurveyPoint of rawSurveyPoints"
|
||||||
|
[rawSurveyPoint]='rawSurveyPoint'
|
||||||
|
[newStatus]="formGroup.value['newStatus']"
|
||||||
|
[canDelete]="formGroup.value['canDelete']"
|
||||||
|
[category]="formGroup.value['destination']"
|
||||||
|
>
|
||||||
|
</gisaf-admin-raw-survey-point>
|
||||||
|
</div>
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Component, OnInit, Input,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
|
||||||
|
|
||||||
|
import { UntypedFormGroup, UntypedFormControl } from '@angular/forms'
|
||||||
|
|
||||||
|
import { map, startWith } from 'rxjs/operators'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
|
import { AdminManageDataService, Category, RawSurveyPoint } from '../data.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-points-by-orig-id',
|
||||||
|
templateUrl: './points-by-orig-id.component.html',
|
||||||
|
styleUrls: ['./points-by-orig-id.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AdminPointsByOrigIdComponent implements OnInit {
|
||||||
|
formGroup: UntypedFormGroup
|
||||||
|
categories: Category[]
|
||||||
|
rawSurveyPoints: RawSurveyPoint[]
|
||||||
|
filteredCategories: Observable<Category[]>
|
||||||
|
destinationControl: UntypedFormControl = new UntypedFormControl()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private adminManageDataService: AdminManageDataService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.formGroup = new UntypedFormGroup({})
|
||||||
|
this.formGroup.addControl('originalId', new UntypedFormControl())
|
||||||
|
this.formGroup.addControl('newStatus', new UntypedFormControl())
|
||||||
|
this.formGroup.addControl('canDelete', new UntypedFormControl())
|
||||||
|
this.formGroup.addControl('destination', this.destinationControl)
|
||||||
|
this.adminManageDataService.getCategories().subscribe(
|
||||||
|
categories => {
|
||||||
|
this.categories = categories
|
||||||
|
this.filteredCategories = this.destinationControl.valueChanges.pipe(
|
||||||
|
startWith(''),
|
||||||
|
map(value => this._filter(value))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filter(value: string): Category[] {
|
||||||
|
const filterValue = value.toUpperCase()
|
||||||
|
return this.categories.filter(
|
||||||
|
category => category.name.indexOf(filterValue) != -1
|
||||||
|
|| category.group.toUpperCase().indexOf(filterValue) != -1
|
||||||
|
|| category.minor_group_1.toUpperCase().indexOf(filterValue) != -1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.adminManageDataService.getRawSurveyPoints(this.formGroup.value['originalId']).subscribe(
|
||||||
|
res => {
|
||||||
|
this.rawSurveyPoints = res
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
8
src/app/admin/admin-manage/project/dialog.component.css
Normal file
8
src/app/admin/admin-manage/project/dialog.component.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
mat-form-field {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
font-family: "GisafSymbols";
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
57
src/app/admin/admin-manage/project/dialog.component.html
Normal file
57
src/app/admin/admin-manage/project/dialog.component.html
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<h1 mat-dialog-title>Edit project</h1>
|
||||||
|
|
||||||
|
<div [formGroup]="formGroup" class='form'>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>id</mat-label>
|
||||||
|
<input matInput formControlName="id" [readonly]="true">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Name</mat-label>
|
||||||
|
<input matInput formControlName="name">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Contact person</mat-label>
|
||||||
|
<input matInput formControlName="contact_person">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Site</mat-label>
|
||||||
|
<input matInput formControlName="site">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Date approved</mat-label>
|
||||||
|
<input matInput formControlName="date_approved" [matDatepicker]="date_approved">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="date_approved"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #date_approved></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Start date planned</mat-label>
|
||||||
|
<input matInput formControlName="start_date_planned" [matDatepicker]="start_date_planned">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="start_date_planned"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #start_date_planned></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Start date effective</mat-label>
|
||||||
|
<input matInput formControlName="start_date_effective" [matDatepicker]="start_date_effective">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="start_date_effective"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #start_date_effective></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>End date planned</mat-label>
|
||||||
|
<input matInput formControlName="end_date_planned" [matDatepicker]="end_date_planned">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="end_date_planned"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #end_date_planned></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>End date effective</mat-label>
|
||||||
|
<input matInput formControlName="end_date_effective" [matDatepicker]="end_date_effective">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="end_date_effective"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #end_date_effective></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="dialogRef.close()">Cancel</button>
|
||||||
|
<button mat-button (click)="save()" type="submit" [disabled]="!formGroup.valid">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
60
src/app/admin/admin-manage/project/dialog.component.ts
Normal file
60
src/app/admin/admin-manage/project/dialog.component.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { Component, OnInit, Input,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy, ViewChild, Inject } from '@angular/core'
|
||||||
|
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'
|
||||||
|
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'
|
||||||
|
|
||||||
|
import { Project } from '../data.service'
|
||||||
|
import { projectFields, ProjectField } from './fields'
|
||||||
|
|
||||||
|
export interface DialogData {
|
||||||
|
project: Project
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-project-dialog',
|
||||||
|
templateUrl: 'dialog.component.html',
|
||||||
|
styleUrls: ['dialog.component.css'],
|
||||||
|
})
|
||||||
|
export class GisafAdminProjectDialogComponent implements OnInit {
|
||||||
|
formGroup: UntypedFormGroup
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<GisafAdminProjectDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: DialogData
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
let project: Project = this.data['project']
|
||||||
|
this.formGroup = new UntypedFormGroup({})
|
||||||
|
//this.formGroup.addControl('id', new FormControl(project.id))
|
||||||
|
for (let projectField of projectFields) {
|
||||||
|
this.formGroup.addControl(
|
||||||
|
projectField.name,
|
||||||
|
new UntypedFormControl(
|
||||||
|
project[projectField.name] || projectField.defaultValue,
|
||||||
|
projectField.validators,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
// Ugly hack for just getting an ISO date like YYYY-MM-DD
|
||||||
|
projectFields.filter(
|
||||||
|
f => f.type == 'date'
|
||||||
|
).forEach(
|
||||||
|
f => {
|
||||||
|
if (this.formGroup.value[f.name] instanceof Date) {
|
||||||
|
let qq = this.formGroup.value[f.name]
|
||||||
|
this.formGroup.value[f.name] = [
|
||||||
|
qq.getFullYear(),
|
||||||
|
qq.getMonth().toFixed().padStart(2, 0),
|
||||||
|
qq.getDate().toFixed().padStart(2, 0)
|
||||||
|
].join('-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.dialogRef.close(this.formGroup.value);
|
||||||
|
}
|
||||||
|
}
|
23
src/app/admin/admin-manage/project/fields.ts
Normal file
23
src/app/admin/admin-manage/project/fields.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Validators } from '@angular/forms'
|
||||||
|
|
||||||
|
export class ProjectField {
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public validators = [],
|
||||||
|
public type: string = 'string',
|
||||||
|
public defaultValue?: string,
|
||||||
|
public inTable: boolean = true
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projectFields: ProjectField[] = [
|
||||||
|
new ProjectField('id'),
|
||||||
|
new ProjectField('name', [Validators.required]),
|
||||||
|
new ProjectField('contact_person'),
|
||||||
|
new ProjectField('site'),
|
||||||
|
new ProjectField('date_approved', [], 'date'),
|
||||||
|
new ProjectField('start_date_planned', [], 'date'),
|
||||||
|
new ProjectField('start_date_effective', [], 'date'),
|
||||||
|
new ProjectField('end_date_planned', [], 'date'),
|
||||||
|
new ProjectField('end_date_effective', [], 'date'),
|
||||||
|
]
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { AdminManageDataService, Project } from '../data.service'
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProjectResolver implements Resolve<object> {
|
||||||
|
constructor(
|
||||||
|
private adminManageDataService: AdminManageDataService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Project[]> {
|
||||||
|
return this.adminManageDataService.getProjects()
|
||||||
|
}
|
||||||
|
}
|
47
src/app/admin/admin-manage/project/project.component.css
Normal file
47
src/app/admin/admin-manage/project/project.component.css
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools button {
|
||||||
|
width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter button {
|
||||||
|
min-width: 1.5em!important;
|
||||||
|
max-width: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.cdk-column-symbol {
|
||||||
|
font-family: "GisafSymbols";
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.even {
|
||||||
|
background-color: #f5f5f512;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unlockCheckbox {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
td .mat-mdc-button {
|
||||||
|
min-width: inherit ! important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-column-delete, .mat-column-edit {
|
||||||
|
width: 3em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.mat-mdc-header-cell:first-of-type, td.mat-mdc-cell:first-of-type, td.mat-mdc-footer-cell:first-of-type {
|
||||||
|
padding-left: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.mat-mdc-header-cell:last-of-type, td.mat-mdc-cell:last-of-type, td.mat-mdc-footer-cell:last-of-type {
|
||||||
|
padding-right: inherit;
|
||||||
|
}
|
74
src/app/admin/admin-manage/project/project.component.html
Normal file
74
src/app/admin/admin-manage/project/project.component.html
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<h1>Survey projects</h1>
|
||||||
|
<div class='tools' fxLayout='row' fxLayoutAlign="space-around center">
|
||||||
|
<mat-form-field class='filter' fxFlex="0 1 0">
|
||||||
|
<mat-label>Filter in table</mat-label>
|
||||||
|
<input matInput
|
||||||
|
(input)="applyFilter()"
|
||||||
|
matTooltip="Filter the items of the basket from the table"
|
||||||
|
[(ngModel)]="filterText"/>
|
||||||
|
<button mat-button matSuffix mat-icon-button
|
||||||
|
*ngIf="filterText"
|
||||||
|
aria-label="Clear"
|
||||||
|
(click)="filterText='';applyFilter()"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
<button mat-raised-button aria-label="New"
|
||||||
|
*ngIf="authenticationService.isAuthorized(edit_roles)"
|
||||||
|
(click)="editItem()">
|
||||||
|
<mat-icon>create</mat-icon>
|
||||||
|
Add new project
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<table mat-table matSort [dataSource]="dataSource" class='content'
|
||||||
|
matSortActive="name" matSortDirection="asc">
|
||||||
|
<ng-container [matColumnDef]="field" *ngFor='let field of fields'>
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ field }}</th>
|
||||||
|
<td mat-cell *matCellDef="let item">{{ item[field] }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="delete"
|
||||||
|
*ngIf="authenticationService.isAuthorized(edit_roles)"
|
||||||
|
>
|
||||||
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
|
<form [formGroup]="unlockDeleteFormGroup" class='unlockCheckbox'>
|
||||||
|
<mat-checkbox formControlName='canDelete' matTooltip='Unlock delete buttons'>
|
||||||
|
</mat-checkbox>
|
||||||
|
</form>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let item">
|
||||||
|
<button mat-button (click)="deleteItem(item)"
|
||||||
|
[disabled]="!unlockDeleteFormGroup.controls['canDelete'].value"
|
||||||
|
matTooltip="Delete"
|
||||||
|
>
|
||||||
|
<mat-icon aria-label="delete">delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="edit">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Edit</th>
|
||||||
|
<td mat-cell *matCellDef="let item">
|
||||||
|
<button mat-button (click)="editItem(item)"
|
||||||
|
matTooltip="Edit the item"
|
||||||
|
>
|
||||||
|
<mat-icon aria-label="edit">edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="columns;sticky:true"></tr>
|
||||||
|
<tr mat-row [ngClass]="{even: even}"
|
||||||
|
*matRowDef="let row; let even = even;columns:columns"
|
||||||
|
></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<mat-paginator
|
||||||
|
class="paginator"
|
||||||
|
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||||
|
[pageIndex]='0'
|
||||||
|
[pageSize]='10'
|
||||||
|
showFirstLastButtons
|
||||||
|
>
|
||||||
|
</mat-paginator>
|
126
src/app/admin/admin-manage/project/project.component.ts
Normal file
126
src/app/admin/admin-manage/project/project.component.ts
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import { Component, OnInit,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy, ViewChild } from '@angular/core'
|
||||||
|
import { UntypedFormGroup, UntypedFormControl } from '@angular/forms'
|
||||||
|
import { ActivatedRoute } from '@angular/router'
|
||||||
|
|
||||||
|
import { MatTableDataSource } from '@angular/material/table'
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar'
|
||||||
|
import { MatSort } from '@angular/material/sort'
|
||||||
|
import { MatPaginator } from '@angular/material/paginator'
|
||||||
|
import { MatDialog } from '@angular/material/dialog'
|
||||||
|
|
||||||
|
import { AdminDataService } from '../../admin-data.service'
|
||||||
|
import { Project, AdminManageDataService } from '../data.service'
|
||||||
|
import { GisafAdminProjectDialogComponent } from './dialog.component'
|
||||||
|
import { AuthenticationService } from '../../../_services/authentication.service'
|
||||||
|
import { ProjectField, projectFields } from './fields'
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-project',
|
||||||
|
templateUrl: './project.component.html',
|
||||||
|
styleUrls: ['./project.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class ProjectComponent implements OnInit {
|
||||||
|
edit_roles = ['project manager']
|
||||||
|
projects: Project[] = []
|
||||||
|
fields: string[] = projectFields.map(f=>f.name)
|
||||||
|
actions: string[] = [
|
||||||
|
'edit',
|
||||||
|
'delete'
|
||||||
|
]
|
||||||
|
columns: string[]
|
||||||
|
dataSource: MatTableDataSource<object>
|
||||||
|
@ViewChild(MatSort, {static: true}) sort: MatSort
|
||||||
|
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator
|
||||||
|
unlockDeleteFormGroup: UntypedFormGroup = new UntypedFormGroup({})
|
||||||
|
filterText: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
public authenticationService: AuthenticationService,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
|
private dataService: AdminDataService,
|
||||||
|
private adminManageDataService: AdminManageDataService,
|
||||||
|
public dialog: MatDialog,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.route.data.subscribe(
|
||||||
|
data => {
|
||||||
|
this.projects = data['projects']
|
||||||
|
this.dataSource = new MatTableDataSource(this.projects)
|
||||||
|
this.dataSource.sort = this.sort
|
||||||
|
this.dataSource.paginator = this.paginator
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.unlockDeleteFormGroup = new UntypedFormGroup({
|
||||||
|
'canDelete': new UntypedFormControl(),
|
||||||
|
})
|
||||||
|
this.columns = projectFields.filter(f=>f.inTable).map(f=>f.name)
|
||||||
|
if (this.authenticationService.isAuthorized(this.edit_roles)) {
|
||||||
|
this.columns = this.columns.concat(this.actions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter() {
|
||||||
|
this.dataSource.filter = this.filterText.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
editItem(data?: Project) {
|
||||||
|
const dialogRef = this.dialog.open(GisafAdminProjectDialogComponent, {
|
||||||
|
width: '75%',
|
||||||
|
data: {
|
||||||
|
'project': data || new Project(undefined, ''),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dialogRef.afterClosed().subscribe(
|
||||||
|
(project: Project) => project &&
|
||||||
|
this.adminManageDataService.saveProject(
|
||||||
|
project
|
||||||
|
).subscribe(
|
||||||
|
(id: number) => {
|
||||||
|
if (data) {
|
||||||
|
// Update
|
||||||
|
let row = this.dataSource.data.findIndex(c=>c['id'] == id)
|
||||||
|
this.dataSource.data[row] = project
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// New
|
||||||
|
project['id'] = id
|
||||||
|
this.dataSource.data.push(project as Project)
|
||||||
|
}
|
||||||
|
this.dataSource.data = this.dataSource.data
|
||||||
|
this.dataService.getSurveyMeta().subscribe()
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
// Popup the dialog again, the error is catched by Apollo
|
||||||
|
this.editItem(project)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteItem(project: Project) {
|
||||||
|
this.adminManageDataService.deleteProject(project).subscribe(
|
||||||
|
_ => {
|
||||||
|
this.snackBar.open(
|
||||||
|
'Project ' + project.name + ' deleted',
|
||||||
|
'Close',
|
||||||
|
{duration: 3000}
|
||||||
|
)
|
||||||
|
// Remove from table
|
||||||
|
let dsi = this.dataSource.data.findIndex(row => row['id'] == project.id)
|
||||||
|
this.dataSource.data.splice(dsi, 1)
|
||||||
|
// Force Angular change detection (??)
|
||||||
|
this.dataSource.data = this.dataSource.data
|
||||||
|
this.dataService.getSurveyMeta().subscribe()
|
||||||
|
this.cdr.markForCheck()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
.container {
|
||||||
|
border: 1px solid grey;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 3px;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id {
|
||||||
|
width: 15em;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
width: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
width: 25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button {
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
<div class='container' fxLayout fxLayoutAlign='center start'>
|
||||||
|
<div fxFlex class='info'>
|
||||||
|
<div class='id'>
|
||||||
|
Database id: {{ rawSurveyPoint.id }}
|
||||||
|
</div>
|
||||||
|
<div class='category'>
|
||||||
|
Survey category: {{ rawSurveyPoint.category }} ({{ rawSurveyPoint.store }})
|
||||||
|
</div>
|
||||||
|
<div class='date'>
|
||||||
|
Survey date: {{ rawSurveyPoint.date }}
|
||||||
|
</div>
|
||||||
|
<div class='status'>
|
||||||
|
Status: {{ rawSurveyPoint.status }}
|
||||||
|
</div>
|
||||||
|
<div class='date'>
|
||||||
|
Type: {{ rawSurveyPoint.type }}
|
||||||
|
</div>
|
||||||
|
<div class='project'>
|
||||||
|
Project: {{ rawSurveyPoint.project }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='buttons' fxFlex='1 1 60%'>
|
||||||
|
<button class='action' mat-raised-button color='secondary'
|
||||||
|
[disabled]='!newStatus'
|
||||||
|
(click)="changeStatus($event)">
|
||||||
|
Change status
|
||||||
|
<mat-icon aria-label="change_status">swap_horiz</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button class='action' mat-raised-button color='secondary'
|
||||||
|
[disabled]='!category'
|
||||||
|
(click)="reconcile($event)">
|
||||||
|
Reconcile
|
||||||
|
<mat-icon aria-label="reconcile">forward</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button class='action' mat-raised-button color='primary'
|
||||||
|
[disabled]='!canDelete'
|
||||||
|
(click)="delete($event)">
|
||||||
|
Delete
|
||||||
|
<mat-icon aria-label="delete">delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Component, OnInit, Input,
|
||||||
|
ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
|
||||||
|
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar'
|
||||||
|
|
||||||
|
import { AdminManageDataService, RawSurveyPoint, Category } from '../data.service'
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-raw-survey-point',
|
||||||
|
templateUrl: './raw-survey-point.component.html',
|
||||||
|
styleUrls: ['./raw-survey-point.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class RawSurveyPointComponent {
|
||||||
|
@Input() rawSurveyPoint: RawSurveyPoint
|
||||||
|
@Input() newStatus: string
|
||||||
|
@Input() category: string
|
||||||
|
@Input() canDelete: boolean
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private adminManageDataService: AdminManageDataService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
changeStatus(evt) {
|
||||||
|
this.adminManageDataService.changeStatus(this.rawSurveyPoint, this.newStatus).subscribe(
|
||||||
|
(res: Object) => {
|
||||||
|
this.snackBar.open(res['message'], 'OK')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(evt) {
|
||||||
|
this.adminManageDataService.delete(this.rawSurveyPoint).subscribe(
|
||||||
|
(res: Object) => {
|
||||||
|
this.snackBar.open(res['message'], 'OK')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
reconcile(evt) {
|
||||||
|
this.adminManageDataService.reconcile(this.rawSurveyPoint, this.category).subscribe(
|
||||||
|
(res: Object) => {
|
||||||
|
this.snackBar.open(res['message'], 'OK')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
23
src/app/admin/admin-menu/admin-menu.component.css
Normal file
23
src/app/admin/admin-menu/admin-menu.component.css
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*@import '../node_modules/@angular/material/prebuilt-themes/purple-green.css';*/
|
||||||
|
|
||||||
|
.active {
|
||||||
|
color: #efe00b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-expansion-panel-header {
|
||||||
|
font-weight: 900;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:host ::ng-deep .mat-expansion-panel-body {
|
||||||
|
padding: 0 10px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host::ng-deep .mat-mdc-list-item-content, :host::ng-deep .mat-mdc-list-item-content button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-expansion-panel-header-description {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
159
src/app/admin/admin-menu/admin-menu.component.html
Normal file
159
src/app/admin/admin-menu/admin-menu.component.html
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
<mat-accordion>
|
||||||
|
<mat-expansion-panel [expanded]=true>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
Baskets
|
||||||
|
</mat-panel-title>
|
||||||
|
<mat-panel-description>
|
||||||
|
<mat-icon>shopping-basket</mat-icon>
|
||||||
|
</mat-panel-description>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<mat-list-item *ngFor="let basket of baskets">
|
||||||
|
<button mat-button
|
||||||
|
matTooltip="{{ basket['name'] }}"
|
||||||
|
routerLink="/admin/basket/{{ basket['name'] }}"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>
|
||||||
|
{{ basket['name'] }}
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
|
||||||
|
<mat-expansion-panel>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
Survey
|
||||||
|
</mat-panel-title>
|
||||||
|
<mat-panel-description>
|
||||||
|
<mat-icon>gps_fixed</mat-icon>
|
||||||
|
</mat-panel-description>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<mat-list-item>
|
||||||
|
<button mat-button
|
||||||
|
routerLink="/admin/survey/categories"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>
|
||||||
|
Categories
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
|
||||||
|
<mat-expansion-panel>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
Scheduler
|
||||||
|
</mat-panel-title>
|
||||||
|
<mat-panel-description>
|
||||||
|
<mat-icon>alarm</mat-icon>
|
||||||
|
</mat-panel-description>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<mat-list-item>
|
||||||
|
<button mat-button
|
||||||
|
routerLink="/admin/scheduler/jobs"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>
|
||||||
|
Jobs
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
<mat-list-item>
|
||||||
|
<button mat-button
|
||||||
|
routerLink="/admin/scheduler/messages"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>
|
||||||
|
Messages
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
|
||||||
|
<mat-expansion-panel
|
||||||
|
*ngIf="authenticationService.isAuthorized(['manager'])"
|
||||||
|
>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
Manage
|
||||||
|
</mat-panel-title>
|
||||||
|
<mat-panel-description>
|
||||||
|
<mat-icon>settings</mat-icon>
|
||||||
|
</mat-panel-description>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
|
||||||
|
<mat-list-item>
|
||||||
|
<button mat-button
|
||||||
|
routerLink="/admin/manage/projects"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
|
||||||
|
<mat-list-item class='root'
|
||||||
|
*ngIf="authenticationService.isAuthorized(['manager'])"
|
||||||
|
>
|
||||||
|
<button mat-button
|
||||||
|
routerLink="/admin/manage/access"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>
|
||||||
|
Access
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
|
||||||
|
<mat-list-item>
|
||||||
|
<button mat-button
|
||||||
|
routerLink="/admin/manage/integrity"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>
|
||||||
|
Integrity
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
|
||||||
|
<mat-list-item>
|
||||||
|
<button mat-button
|
||||||
|
routerLink="/admin/manage/maintenance"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>
|
||||||
|
Maintenance
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
|
||||||
|
<mat-list-item>
|
||||||
|
<button mat-button
|
||||||
|
routerLink="/admin/manage/points-by-orig-id"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>
|
||||||
|
Points by orig. ID
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
|
||||||
|
</mat-expansion-panel>
|
||||||
|
|
||||||
|
<!-- Models are currently disabled, need fixes
|
||||||
|
<mat-expansion-panel [disabled]=true>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
Models
|
||||||
|
</mat-panel-title>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
|
||||||
|
<mat-expansion-panel
|
||||||
|
*ngFor="let menuItem of adminModelsMenuItems"
|
||||||
|
>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
{{ menuItem['name'] }}
|
||||||
|
</mat-panel-title>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<mat-list-item
|
||||||
|
*ngFor="let item of menuItem['items']"
|
||||||
|
>
|
||||||
|
<button mat-button
|
||||||
|
matTooltip="{{ item['name'] }}"
|
||||||
|
routerLink="/admin/model/{{ item['name'] }}"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>
|
||||||
|
{{ item['name'] }}
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
-->
|
||||||
|
</mat-accordion>
|
25
src/app/admin/admin-menu/admin-menu.component.spec.ts
Normal file
25
src/app/admin/admin-menu/admin-menu.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AdminMenuComponent } from './admin-menu.component';
|
||||||
|
|
||||||
|
describe('AdminMenuComponent', () => {
|
||||||
|
let component: AdminMenuComponent;
|
||||||
|
let fixture: ComponentFixture<AdminMenuComponent>;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ AdminMenuComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdminMenuComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
17
src/app/admin/admin-menu/admin-menu.component.ts
Normal file
17
src/app/admin/admin-menu/admin-menu.component.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
|
||||||
|
import { AuthenticationService } from '../../_services/authentication.service'
|
||||||
|
import { AdminBasket } from '../admin-basket/data.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gisaf-admin-menu',
|
||||||
|
templateUrl: './admin-menu.component.html',
|
||||||
|
styleUrls: ['./admin-menu.component.css'],
|
||||||
|
})
|
||||||
|
export class AdminMenuComponent {
|
||||||
|
@Input() adminModelsMenuItems: object[]
|
||||||
|
@Input() baskets: AdminBasket[]
|
||||||
|
constructor(
|
||||||
|
public authenticationService: AuthenticationService
|
||||||
|
) {}
|
||||||
|
}
|
29
src/app/admin/admin-resolver.service.ts
Normal file
29
src/app/admin/admin-resolver.service.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'
|
||||||
|
import { Observable, forkJoin } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
|
import { AdminDataService } from './admin-data.service'
|
||||||
|
import { SurveyMeta } from './admin-data.service'
|
||||||
|
import { AdminBasketDataService, AdminBasket } from './admin-basket/data.service'
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminResolver implements Resolve<object> {
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private dataService: AdminDataService,
|
||||||
|
private basketDataService: AdminBasketDataService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot,
|
||||||
|
): Observable<[AdminBasket[], SurveyMeta/*, object[]*/]> {
|
||||||
|
return forkJoin(
|
||||||
|
this.basketDataService.getBaskets(),
|
||||||
|
this.dataService.getSurveyMeta(),
|
||||||
|
//this.dataService.getModelsMenuBar(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
105
src/app/admin/admin-routing.module.ts
Normal file
105
src/app/admin/admin-routing.module.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
|
|
||||||
|
import { AdminComponent } from './admin.component'
|
||||||
|
import { AdminAccessComponent } from './admin-manage/access/users-roles.component'
|
||||||
|
import { AdminListComponent } from './admin-list/admin-list.component'
|
||||||
|
import { AdminDetailComponent } from './admin-detail/admin-detail.component'
|
||||||
|
import { AdminBasketComponent } from './admin-basket/basket.component'
|
||||||
|
import { AdminPointsByOrigIdComponent } from './admin-manage/points-by-orig-id/points-by-orig-id.component'
|
||||||
|
import { AdminResolver } from './admin-resolver.service'
|
||||||
|
import { DetailResolver } from './admin-detail/admin-detail-resolver.service'
|
||||||
|
import { BasketResolver } from './admin-basket/admin-basket-resolver.service'
|
||||||
|
import { CategoryComponent } from './admin-manage/category/category.component'
|
||||||
|
import { CategoryResolver } from './admin-manage/category/category-resolver.service'
|
||||||
|
import { ProjectComponent } from './admin-manage/project/project.component'
|
||||||
|
import { ProjectResolver } from './admin-manage/project/project-resolver.service'
|
||||||
|
import { AdminIntegrityComponent } from './admin-manage/integrity/integrity.component'
|
||||||
|
import { AdminMaintenanceComponent } from './admin-manage/maintenance/maintenance.component'
|
||||||
|
import { AdminSchedulerJobsComponent } from './admin-scheduler/jobs.component'
|
||||||
|
import { AdminSchedulerMessagesComponent } from './admin-scheduler/messages.component'
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: AdminComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'basket/:name',
|
||||||
|
component: AdminBasketComponent,
|
||||||
|
resolve: {
|
||||||
|
basket: BasketResolver,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'manage/access',
|
||||||
|
component: AdminAccessComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'survey/categories',
|
||||||
|
component: CategoryComponent,
|
||||||
|
resolve: {
|
||||||
|
categories: CategoryResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'scheduler/jobs',
|
||||||
|
component: AdminSchedulerJobsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'scheduler/messages',
|
||||||
|
component: AdminSchedulerMessagesComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'manage/projects',
|
||||||
|
component: ProjectComponent,
|
||||||
|
resolve: {
|
||||||
|
projects: ProjectResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'manage/integrity',
|
||||||
|
component: AdminIntegrityComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'manage/maintenance',
|
||||||
|
component: AdminMaintenanceComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'manage/points-by-orig-id',
|
||||||
|
component: AdminPointsByOrigIdComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'model/:modelName',
|
||||||
|
component: AdminListComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'model/:modelName/:pk',
|
||||||
|
component: AdminDetailComponent,
|
||||||
|
resolve: {
|
||||||
|
item: DetailResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
item: AdminResolver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
providers: [
|
||||||
|
AdminResolver,
|
||||||
|
//AdminAccessResolver,
|
||||||
|
BasketResolver,
|
||||||
|
CategoryResolver,
|
||||||
|
ProjectResolver,
|
||||||
|
DetailResolver,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminRoutingModule { }
|
8
src/app/admin/admin-scheduler/feature.component.css
Normal file
8
src/app/admin/admin-scheduler/feature.component.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.container {
|
||||||
|
margin: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: #212121;
|
||||||
|
cursor: pointer;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
5
src/app/admin/admin-scheduler/feature.component.html
Normal file
5
src/app/admin/admin-scheduler/feature.component.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<div class='container'
|
||||||
|
matTooltip='View on map'
|
||||||
|
(click)='show()'>
|
||||||
|
{{ feature.store }} #{{ feature.id }}
|
||||||
|
</div>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue