Initial commit for gisaf/fastapi

This commit is contained in:
phil 2024-02-17 12:35:03 +05:30
commit adce44722f
1361 changed files with 42521 additions and 0 deletions

12
.browserslistrc Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

87
package.json Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,7 @@
export class User {
constructor(
public userName: string,
public token: string,
public password?: string,
) {}
}

View 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']
)
)
)
)
)
)
)
)
)
)
)
)
))
}
}

View 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
}
))
}
}

View 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();
}));
});

View 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
}
}

View 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();
}));
});

View 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()
}
}

View 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'
}
)
}
}

View 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()
}
}

View 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)
}
}

View file

@ -0,0 +1,6 @@
.item {
border: 1px solid grey;
margin: 0 3px;
padding: 0 2px;
border-radius: 3px;
}

View file

@ -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>

View file

@ -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() {
}
}

View 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)
}
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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
}
}

View 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;
}

View 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>

View 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)
}
}

View 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']
))
}
}

View 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
}
))
}
}

View 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
}
}
))
}
}

View 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;
}

View 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>

View 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)
}
)
}
}

View 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 { }

View 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>

View 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()
})
}
}

View file

@ -0,0 +1 @@
/*@import '../node_modules/@angular/material/prebuilt-themes/purple-green.css';*/

View 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>

View 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 {}

View 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;
}

View 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>

View 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();
});
});

View 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() {
}
}

View 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)
}
})
)
}
}

View 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[],
) {}
}

View 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>

View 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);
}
}

View 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;
}

View 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>

View 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)
}
}

View file

@ -0,0 +1,3 @@
.mat-mdc-dialog-content {
height: 20em;
}

View 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>

View 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);
}
}

View 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;
}

View 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>

View 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)
}
}

View 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;
}

View 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>

View 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()
}
}

View file

@ -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()
}
}

View 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;
}

View 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>

View 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}
)
}
)
}
}

View file

@ -0,0 +1,8 @@
mat-form-field {
padding: 0 0.5em;
}
.symbol {
font-family: "GisafSymbols";
font-size: 2em;
}

View 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>

View 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);
}
}

View 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),
]

View 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']
)
)
}
))
}
}

View 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;
}

View file

@ -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>

View 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()
}
}

View file

@ -0,0 +1,4 @@
<button mat-button (click)="vacuumDb()">
<mat-icon>healing</mat-icon>
Vacuum DB
</button>

View file

@ -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',
)
)
}
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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()
}
)
}
}

View file

@ -0,0 +1,8 @@
mat-form-field {
padding: 0 0.5em;
}
.symbol {
font-family: "GisafSymbols";
font-size: 2em;
}

View 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>

View 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);
}
}

View 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'),
]

View file

@ -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()
}
}

View 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;
}

View 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>

View 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()
}
)
}
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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')
}
)
}
}

View 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;
}

View 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>

View 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();
});
});

View 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
) {}
}

View 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(),
)
}
}

View 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 { }

View file

@ -0,0 +1,8 @@
.container {
margin: 2px;
padding: 2px;
border-radius: 2px;
background-color: #212121;
cursor: pointer;
width: fit-content;
}

View 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