Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion instance/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngageoint/mage.dev-instance",
"version": "6.6.4",
"version": "6.6.7",
"description": "Assemble a Mage Server deployment from the core service, the web-app, and selected plugins. This is primarily a development tool because the dependencies point to relative directories instead of production packages. This can however serve as a starting point to create a production Mage instance package.json.",
"scripts": {
"start": "npm run start:dev",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@ngageoint/mage.project",
"description": "This is the root package definition for the mage-server monorepo.",
"private": true,
"version": "6.6.4",
"version": "6.6.7",
"files": [],
"scripts": {
"postinstall": "npm-run-all service:ci web-app:ci arcgis:ci sftp:ci nga-msi:ci",
Expand Down
4 changes: 2 additions & 2 deletions service/npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion service/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngageoint/mage.service",
"version": "6.6.4",
"version": "6.6.7",
"displayName": "Mage Service",
"description": "Mage is a geospatial situational awareness and data collection platform. The Mage Service is the ReST service API that the Mage client apps use to interact with Mage data.",
"keywords": [
Expand Down
7 changes: 6 additions & 1 deletion service/src/api/observation.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,14 @@ Observation.prototype.validate = function (observation) {
let fieldsMessage = "";
const fieldsError = {};

formDefinitions[formEntry.formId].fields
const formDefinition = formDefinitions[formEntry.formId];
const userFieldNames = new Set(formDefinition.userFields || []);
formDefinition.fields
.filter((fieldDefinition) => !fieldDefinition.archived)
.forEach((fieldDefinition) => {
// User fields have dynamically populated choices that are not stored in the
// form definition, so skip choice-based validation for those fields.
if (userFieldNames.has(fieldDefinition.name)) return;
const field = fieldFactory.createField(
fieldDefinition,
formEntry,
Expand Down
7 changes: 6 additions & 1 deletion service/src/entities/observations/entities.observations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1184,10 +1184,15 @@ function validateFormFieldEntries(formEntry: FormEntry, form: Form, formEntryErr
const { mageEvent, observationAttrs } = validation
const formFields = form.fields || []
const activeFields = formFields.filter(x => !x.archived)
const userFields = new Set(form.userFields || [])
activeFields.forEach(field => {
const fieldEntry = formEntry[field.name]
const fieldValidation: FormFieldValidationContext = { field, fieldEntry, formEntry, mageEvent, observationAttrs }
const resultEntry = FieldTypeValidationRules[field.type](fieldValidation)
const isUserField = userFields.has(field.name)
const rule = isUserField
? validateRequiredThen(context => fields.text.TextFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context)))
: FieldTypeValidationRules[field.type]
const resultEntry = rule(fieldValidation)
if (resultEntry instanceof FormFieldValidationError) {
formEntryError.addFieldError(resultEntry)
}
Expand Down
212 changes: 148 additions & 64 deletions web-app/admin/src/app/admin/admin-dashboard/admin-dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,104 +2,188 @@

<div class="admin-content p-4">
<mat-card class="mb-4 top-card">
<div>
<div class="top-row">
<div class="inactive-users">
<mat-card>
<mat-card-title>
<span class="title-with-badge">
Inactive Users
<span matBadge="{{ count() || 0 }}" matBadgeColor="accent" matBadgeOverlap="false"
class="inline-badge"></span>
</span>
</mat-card-title>

<div class="search-wrapper">
<mat-form-field class="w-100 mb-3">
<mat-label>Search</mat-label>
<input matInput [(ngModel)]="userSearch" (ngModelChange)="search()" />
</mat-form-field>
</div>
<div class="top-row">
<div class="dashboard-panel inactive-users">
<mat-card class="dashboard-panel-card">
<mat-card-title>
<span class="title-with-badge">
Inactive Users
<span
matBadge="{{ count() || 0 }}"
matBadgeColor="accent"
matBadgeOverlap="false"
class="inline-badge"
></span>
</span>
</mat-card-title>

<div class="search-wrapper">
<mat-form-field class="w-100 mb-3">
<mat-label>Search</mat-label>
<input
matInput
[(ngModel)]="userSearch"
(ngModelChange)="search()"
/>
</mat-form-field>
</div>

<div class="panel-list-shell">
<mat-list>
<mat-list-item *ngFor="let user of inactiveUsers || []" class="clickable-row"
[routerLink]="['../users', user?.id]">
<mat-list-item
*ngFor="let user of inactiveUsers || []"
class="clickable-row"
[routerLink]="['../users', user?.id]"
>
<mat-icon matListIcon>person</mat-icon>

<div matLine>{{ user?.displayName || 'Unknown User' }}</div>

<button mat-button color="primary" *ngIf="hasPermission('UPDATE_USER')" matListItemMeta type="button"
<button
mat-button
color="primary"
*ngIf="hasPermission('UPDATE_USER')"
matListItemMeta
type="button"
(mousedown)="$event.preventDefault(); $event.stopPropagation(); $event.stopImmediatePropagation()"
(click)="$event.preventDefault(); $event.stopPropagation(); $event.stopImmediatePropagation(); activateUser(user)">
(click)="$event.preventDefault(); $event.stopPropagation(); $event.stopImmediatePropagation(); activateUser(user)"
>
<mat-icon>check</mat-icon>
<span>Activate</span>
</button>
</mat-list-item>
</mat-list>

<div class="pagination-controls">
<button mat-button (click)="previous()" [disabled]="!hasPrevious()" type="button">
← Previous User
</button>
<button mat-button (click)="next()" [disabled]="!hasNext()" type="button">
Next User →
</button>
</div>
</mat-card>
</div>

<div class="unregistered-devices">
<mat-card>
<mat-card-title>
<span class="title-with-badge">
Unregistered Devices
<span matBadge="{{ deviceCount() || 0 }}" matBadgeColor="accent" matBadgeOverlap="false"
class="inline-badge"></span>
</span>
</mat-card-title>

<div class="search-wrapper">
<mat-form-field class="w-100 mb-3">
<mat-label>Search</mat-label>
<input matInput [(ngModel)]="deviceSearch" (ngModelChange)="searchDevices()" />
</mat-form-field>
<div
class="empty-panel-state"
*ngIf="!(inactiveUsers && inactiveUsers.length)"
>
No inactive users found.
</div>
</div>

<div class="pagination-controls">
<button
mat-button
class="pagination-button"
(click)="previous()"
[disabled]="!hasPrevious() || loadingUsersPage"
type="button"
>
← Previous User
</button>

<button
mat-button
class="pagination-button"
(click)="next()"
[disabled]="!hasNext() || loadingUsersPage"
type="button"
>
Next User →
</button>
</div>
</mat-card>
</div>

<div class="dashboard-panel unregistered-devices">
<mat-card class="dashboard-panel-card">
<mat-card-title>
<span class="title-with-badge">
Unregistered Devices
<span
matBadge="{{ deviceCount() || 0 }}"
matBadgeColor="accent"
matBadgeOverlap="false"
class="inline-badge"
></span>
</span>
</mat-card-title>

<div class="search-wrapper">
<mat-form-field class="w-100 mb-3">
<mat-label>Search</mat-label>
<input
matInput
[(ngModel)]="deviceSearch"
(ngModelChange)="searchDevices()"
/>
</mat-form-field>
</div>

<div class="panel-list-shell">
<mat-list>
<mat-list-item *ngFor="let d of unregisteredDevices || []" class="clickable-row" (click)="goToDevice(d)">
<mat-list-item
*ngFor="let d of unregisteredDevices || []"
class="clickable-row"
(click)="goToDevice(d)"
>
<i matListIcon [ngClass]="iconClass(d)"></i>

<div class="mat-line-horizontal" matLine>
<span *ngIf="d?.user">
{{ d.user?.displayName || 'Unknown User' }}
</span>
<span class="text-muted">({{ d?.uid || 'Unknown UID' }})</span>
<span class="text-muted">
({{ d?.uid || 'Unknown UID' }})
</span>
</div>

<button mat-button color="primary" *ngIf="hasPermission('UPDATE_DEVICE')"
(click)="registerDevice($event, d)" matListItemMeta type="button">
<mat-icon>check</mat-icon> Register
<button
mat-button
color="primary"
*ngIf="hasPermission('UPDATE_DEVICE')"
matListItemMeta
type="button"
(mousedown)="$event.preventDefault(); $event.stopPropagation(); $event.stopImmediatePropagation()"
(click)="$event.preventDefault(); $event.stopPropagation(); $event.stopImmediatePropagation(); registerDevice($event, d)"
>
<mat-icon>check</mat-icon>
<span>Register</span>
</button>
</mat-list-item>
</mat-list>

<div class="pagination-controls">
<button mat-button (click)="previousDevice()" [disabled]="!hasPreviousDevice()" type="button">
← Previous Device
</button>
<button mat-button (click)="nextDevice()" [disabled]="!hasNextDevice()" type="button">
Next Device →
</button>
<div
class="empty-panel-state"
*ngIf="!(unregisteredDevices && unregisteredDevices.length)"
>
No unregistered devices found.
</div>
</mat-card>
</div>
</div>

<div class="pagination-controls">
<button
mat-button
class="pagination-button"
(click)="previousDevice()"
[disabled]="!hasPreviousDevice() || loadingDevicesPage"
type="button"
>
← Previous Device
</button>

<button
mat-button
class="pagination-button"
(click)="nextDevice()"
[disabled]="!hasNextDevice() || loadingDevicesPage"
type="button"
>
Next Device →
</button>
</div>
</mat-card>
</div>
</div>
</mat-card>

<div class="section-card">
<div class="table-wrapper mat-elevation-z2">
<mage-logins [devices]="unregisteredDevices" [users]="inactiveUsers"></mage-logins>
<mage-logins
[devices]="unregisteredDevices"
[users]="inactiveUsers"
></mage-logins>
</div>
</div>
</div>
</div>
Loading
Loading