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