diff --git a/plugins/arcgis/service/src/ArcGISConfig.ts b/plugins/arcgis/service/src/ArcGISConfig.ts index 91ff97475..5451fcc27 100644 --- a/plugins/arcgis/service/src/ArcGISConfig.ts +++ b/plugins/arcgis/service/src/ArcGISConfig.ts @@ -32,7 +32,6 @@ export interface FeatureServiceConfig { * The feature layers. */ layers: FeatureLayerConfig[] - } /** @@ -125,7 +124,7 @@ export interface OAuthAuthConfig { /** * The expiration date for the temporary token */ - authTokenExpires?: string + authTokenExpires?: number /** * The Refresh token for OAuth @@ -135,7 +134,7 @@ export interface OAuthAuthConfig { /** * The expiration date for the Refresh token */ - refreshTokenExpires?: string + refreshTokenExpires?: number } /** diff --git a/plugins/arcgis/service/src/ArcGISPluginConfig.ts b/plugins/arcgis/service/src/ArcGISPluginConfig.ts index 2d5592c20..831ce8b9a 100644 --- a/plugins/arcgis/service/src/ArcGISPluginConfig.ts +++ b/plugins/arcgis/service/src/ArcGISPluginConfig.ts @@ -10,6 +10,11 @@ export interface ArcGISPluginConfig { */ enabled: boolean + /** + * Mage base server url + */ + baseUrl: string + /** * Query the database for new observations to process at the given * repeating time interval in seconds. @@ -128,7 +133,8 @@ export interface ArcGISPluginConfig { } export const defaultArcGISPluginConfig = Object.freeze({ - enabled: true, + enabled: false, + baseUrl: '', intervalSeconds: 60, startupIntervalSeconds: 1, updateIntervalSeconds: 1, diff --git a/plugins/arcgis/service/src/FeatureService.ts b/plugins/arcgis/service/src/FeatureService.ts index d9162430b..32323adeb 100644 --- a/plugins/arcgis/service/src/FeatureService.ts +++ b/plugins/arcgis/service/src/FeatureService.ts @@ -1,83 +1,153 @@ -import { LayerInfoResult} from "./LayerInfoResult"; -import { FeatureServiceResult} from "./FeatureServiceResult"; +import { LayerInfoResult } from "./LayerInfoResult"; +import { FeatureServiceResult } from "./FeatureServiceResult"; import { HttpClient } from "./HttpClient"; -import { FeatureServiceConfig, FeatureLayerConfig } from "./ArcGISConfig"; +import { AuthType, FeatureServiceConfig } from "./ArcGISConfig"; +import { ArcGISIdentityManager } from "@esri/arcgis-rest-request"; +import { URL } from "node:url" /** * Queries arc feature services and layers. */ export class FeatureService { - /** - * Used to make the get request about the feature layer. - */ - private _httpClient: HttpClient; + /** + * Used to make the get request about the feature layer. + */ + private _httpClient: HttpClient; - /** - * Used to log messages. - */ - private _console: Console; + /** + * Used to log messages. + */ + private _console: Console; - /** - * Constructor. - * @param console Used to log messages. - * @param token The access token. - */ - constructor(console: Console, token?: string) { - this._httpClient = new HttpClient(console, token); - this._console = console; - } + /** + * Constructor. + * @param console Used to log messages. + * @param token The access token. + */ + constructor(console: Console, token?: string) { + this._httpClient = new HttpClient(console, token); + this._console = console; + } - /** - * Queries an arc feature service. - * @param url The url to the arc feature layer. - * @param callback Function to call once response has been received and parsed. - */ - queryFeatureService(url: string, callback: (featureService: FeatureServiceResult) => void) { - this._httpClient.sendGetHandleResponse(url, this.parseFeatureService(url, callback)) - } + /** + * Queries an arc feature service. + * @param url The url to the arc feature layer. + * @param callback Function to call once response has been received and parsed. + */ + queryFeatureService(url: string, callback: (featureService: FeatureServiceResult) => void) { + this._httpClient.sendGetHandleResponse(url, this.parseFeatureService(url, callback)) + } - /** - * Parses the response from the feature service request and sends to the callback. - * @param url The url to the arc feature layer. - * @param callback The callback to call and send the feature service to. - */ - private parseFeatureService(url: string, callback: (featureService: FeatureServiceResult) => void) { - return (chunk: any) => { - this._console.log('Feature Service. url: ' + url + ', response: ' + chunk) - try { - const service = JSON.parse(chunk) as FeatureServiceResult - callback(service) - } catch(e) { - this._console.error(e) - } - } - } + /** + * Parses the response from the feature service request and sends to the callback. + * @param url The url to the arc feature layer. + * @param callback The callback to call and send the feature service to. + */ + private parseFeatureService(url: string, callback: (featureService: FeatureServiceResult) => void) { + return (chunk: any) => { + this._console.log('Feature Service. url: ' + url + ', response: ' + chunk) + try { + const service = JSON.parse(chunk) as FeatureServiceResult + callback(service) + } catch (e) { + this._console.error(e) + } + } + } - /** - * Queries an arc feature layer to get info on the layer. - * @param url The url to the arc feature layer. - * @param infoCallback Function to call once response has been received and parsed. - */ - queryLayerInfo(url: string, infoCallback: (layerInfo: LayerInfoResult) => void) { - this._httpClient.sendGetHandleResponse(url, this.parseLayerInfo(url, infoCallback)); - } + /** + * Queries an arc feature layer to get info on the layer. + * @param url The url to the arc feature layer. + * @param infoCallback Function to call once response has been received and parsed. + */ + queryLayerInfo(url: string, infoCallback: (layerInfo: LayerInfoResult) => void) { + this._httpClient.sendGetHandleResponse(url, this.parseLayerInfo(url, infoCallback)); + } - /** - * Parses the response from the request and sends the layer info to the callback. - * @param url The url to the feature layer. - * @param infoCallback The callback to call and send the layer info to. - */ - private parseLayerInfo(url: string, infoCallback: (layerInfo: LayerInfoResult) => void) { - return (chunk: any) => { - this._console.log('Query Layer. url: ' + url + ', response: ' + chunk) - try { - const layerInfo = JSON.parse(chunk) as LayerInfoResult - infoCallback(layerInfo) - } catch(e) { - this._console.error(e) - } - } - } + /** + * Parses the response from the request and sends the layer info to the callback. + * @param url The url to the feature layer. + * @param infoCallback The callback to call and send the layer info to. + */ + private parseLayerInfo(url: string, infoCallback: (layerInfo: LayerInfoResult) => void) { + return (chunk: any) => { + this._console.log('Query Layer. url: ' + url + ', response: ' + chunk) + try { + const layerInfo = JSON.parse(chunk) as LayerInfoResult + infoCallback(layerInfo) + } catch (e) { + this._console.error(e) + } + } + } +} +export function getPortalUrl(featureService: FeatureServiceConfig | string): string { + const url = getFeatureServiceUrl(featureService) + return `https://${new URL(url).hostname}/arcgis/sharing/rest` +} + +export function getServerUrl(featureService: FeatureServiceConfig | string): string { + const url = getFeatureServiceUrl(featureService) + return `https://${url.hostname}/arcgis` +} + +export function getFeatureServiceUrl(featureService: FeatureServiceConfig | string): URL { + const url = typeof featureService === 'string' ? featureService : featureService.url + return new URL(url) +} + +export async function getIdentityManager( + featureService: FeatureServiceConfig, + httpClient: HttpClient // TODO remove in favor of an open source lib like axios +): Promise { + switch (featureService.auth?.type) { + case AuthType.Token: { + return ArcGISIdentityManager.fromToken({ + token: featureService.auth?.token, + portal: getPortalUrl(featureService), + server: getServerUrl(featureService) + }) + } + case AuthType.UsernamePassword: { + return ArcGISIdentityManager.signIn({ + username: featureService.auth?.username, + password: featureService.auth?.password, + portal: getPortalUrl(featureService), + }) + } + case AuthType.OAuth: { + // Check if feature service has refresh token and use that to generate token to use + const portal = getPortalUrl(featureService) + const { clientId, authToken, authTokenExpires, refreshToken, refreshTokenExpires } = featureService.auth + if (authToken && new Date(authTokenExpires || 0) > new Date()) { + return new ArcGISIdentityManager({ + clientId: clientId, + token: authToken, + tokenExpires: new Date(authTokenExpires || 0), + refreshToken: refreshToken, + refreshTokenExpires: new Date(refreshTokenExpires || 0), + portal: getPortalUrl(featureService), + server: getServerUrl(featureService) + }) + } else { + if (refreshToken && new Date(refreshTokenExpires || 0) > new Date()) { + const url = `${portal}/oauth2/token?client_id=${clientId}&refresh_token=${refreshToken}&grant_type=refresh_token` + const response = await httpClient.sendGet(url) + // TODO: error handling + return ArcGISIdentityManager.fromToken({ + clientId: clientId, + token: response.access_token, + portal: portal + }); + // TODO: update authToken to new token + } else { + // TODO the config, we need to let the user know UI side they need to authenticate again + throw new Error('Refresh token missing or expired') + } + } + } + default: throw new Error('Authentication type not supported') + } } \ No newline at end of file diff --git a/plugins/arcgis/service/src/index.ts b/plugins/arcgis/service/src/index.ts index 40e0c70e5..2592ff221 100644 --- a/plugins/arcgis/service/src/index.ts +++ b/plugins/arcgis/service/src/index.ts @@ -4,21 +4,15 @@ import { ObservationRepositoryToken } from '@ngageoint/mage.service/lib/plugins. import { MageEventRepositoryToken } from '@ngageoint/mage.service/lib/plugins.api/plugins.api.events' import { UserRepositoryToken } from '@ngageoint/mage.service/lib/plugins.api/plugins.api.users' import { SettingPermission } from '@ngageoint/mage.service/lib/entities/authorization/entities.permissions' -import express from 'express' import { ArcGISPluginConfig } from './ArcGISPluginConfig' -import { OAuthAuthConfig, AuthType } from './ArcGISConfig' +import { AuthType } from './ArcGISConfig' import { ObservationProcessor } from './ObservationProcessor' import { HttpClient } from './HttpClient' -import { FeatureServiceResult } from './FeatureServiceResult' -import { ArcGISIdentityManager } from "@esri/arcgis-rest-request" -// import { IQueryFeaturesOptions, queryFeatures } from '@esri/arcgis-rest-feature-service' - -// TODO: Remove hard coded creds -const credentials = { - clientId: 'kbHGOg5BFjYf1sTA', - portal: 'https://arcgis.geointnext.com/arcgis/sharing/rest', - redirectUri: 'http://localhost:4242/plugins/@ngageoint/mage.arcgis.service/oauth/authenticate' -} +import { ArcGISIdentityManager, request } from "@esri/arcgis-rest-request" +import { FeatureServiceConfig } from './ArcGISConfig' +import { URL } from "node:url" +import express from 'express' +import { getIdentityManager, getPortalUrl } from './FeatureService' const logPrefix = '[mage.arcgis]' const logMethods = ['log', 'debug', 'info', 'warn', 'error'] as const @@ -42,107 +36,7 @@ const InjectedServices = { userRepo: UserRepositoryToken } -/** - * Provides the portal URL from a given feature URL. - * - * @param {string} featureServiceUrl - The URL of the feature service. - * @returns {string} The portal URL. - */ -function getPortalUrl(featureServiceUrl: string): string { - const url = new URL(featureServiceUrl); - return `https://${url.hostname}/arcgis/sharing/rest`; -} - -/** - * Provides the server URL from a given feature URL. - * - * @param {string} featureServiceUrl - The URL of the feature service. - * @returns {string} The server URL. - */ -function getServerUrl(featureServiceUrl: string): string { - const url = new URL(featureServiceUrl); - return `https://${url.hostname}/arcgis`; -} - - /** - * Handles authentication for a given request. - * - * @param {express.Request} req The express request object. - * @returns {Promise} The authenticated identity manager. - * - * @throws {Error} If the identity manager could not be created due to missing required query parameters. - */ -async function handleAuthentication(req: express.Request, httpClient: HttpClient, processor: ObservationProcessor): Promise { - const featureUsername = req.query.username as string | undefined; - const featurePassword = req.query.password as string | undefined; - const featureClientId = req.query.clientId as string | undefined; - const featureServer = req.query.server as string | undefined; - const featurePortal = req.query.portal as string | undefined; - const featureToken = req.query.token as string | undefined; - const portalUrl = getPortalUrl(req.query.featureUrl as string ?? ''); - - let identityManager: ArcGISIdentityManager; - - try { - if (featureToken) { - console.log('Token provided for authentication'); - identityManager = await ArcGISIdentityManager.fromToken({ - token: featureToken, - server: getServerUrl(req.query.featureUrl as string ?? ''), - portal: portalUrl - }); - } else if (featureUsername && featurePassword) { - console.log('Username and password provided for authentication, username:' + featureUsername); - identityManager = await ArcGISIdentityManager.signIn({ - username: featureUsername, - password: featurePassword, - portal: portalUrl, - }); - } else if (featureClientId) { - console.log('Client ID provided for authentication'); - - // Check if feature service has refresh token and use that to generate token to use - // Else complain - const config = await processor.safeGetConfig(); - const featureService = config.featureServices.find((service) => service.auth?.type === AuthType.OAuth); - const authToken = (featureService?.auth as OAuthAuthConfig)?.authToken; - const authTokenExpires = (featureService?.auth as OAuthAuthConfig)?.authTokenExpires as string; - if (authToken && new Date(authTokenExpires) > new Date()) { - // TODO: error handling - identityManager = await ArcGISIdentityManager.fromToken({ - clientId: featureClientId, - token: authToken, - portal: portalUrl - }); - } else { - const refreshToken = (featureService?.auth as OAuthAuthConfig)?.refreshToken; - const refreshTokenExpires = (featureService?.auth as OAuthAuthConfig)?.refreshTokenExpires as string; - if (refreshToken && new Date(refreshTokenExpires) > new Date()) { - const url = `${portalUrl}/oauth2/token?client_id=${featureClientId}&refresh_token=${refreshToken}&grant_type=refresh_token` - const response = await httpClient.sendGet(url) - // TODO: error handling - identityManager = await ArcGISIdentityManager.fromToken({ - clientId: featureClientId, - token: response.access_token, - portal: portalUrl - }); - // TODO: update authToken to new token - } else { - throw new Error('Refresh token missing or expired.') - } - } - } else { - throw new Error('Missing required query parameters to authenticate (token or username/password or oauth parameters).'); - } - - console.log('Identity Manager token', identityManager.token); - console.log('Identity Manager token expires', identityManager.tokenExpires); //TODO - is expiration arbitrary from ArcGISIdentityManager or actually the correct expiration allowed by server? Why undefined days later? - } catch (error) { - console.error('Error during authentication:', error); - throw new Error('Authentication failed.'); - } - return identityManager; -} +const pluginWebRoute = "plugins/@ngageoint/mage.arcgis.service" /** * The MAGE ArcGIS Plugin finds new MAGE observations and if configured to send the observations @@ -160,66 +54,85 @@ const arcgisPluginHooks: InitPluginHook = { console.info('Intializing ArcGIS plugin...') const { stateRepo, eventRepo, obsRepoForEvent, userRepo } = services // TODO - // - Move getServerUrl to Helper file - // - Move getPortalUrl to Helper file // - Update layer token to get token from identity manager // - Move plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts addLayer to helper file and use instead of encodeURIComponent - // - Remove Client secret from returned Config object if applicable - const processor = new ObservationProcessor(stateRepo, eventRepo, obsRepoForEvent, userRepo, console); - processor.start(); + const processor = new ObservationProcessor(stateRepo, eventRepo, obsRepoForEvent, userRepo, console) + processor.start() return { webRoutes: { public: (requestContext: GetAppRequestContext) => { - const routes = express.Router().use(express.json()); - routes.get("/oauth/sign-in", async (req, res) => { - const clientId = req.query.clientId as string; - const portal = req.query.portalUrl as string; - const redirectUri = req.query.redirectUrl as string; - // TODO: Replace with better way if possible to pass creds to /oauth/authenticate - const config = await processor.safeGetConfig(); - config.featureServices.push({ - url: portal, - layers: [], - auth: { - type: AuthType.OAuth, - clientId: clientId, - redirectUri: redirectUri - } - }) - await processor.putConfig(config); + const routes = express.Router().use(express.json()) + + routes.get('/oauth/signin', async (req, res) => { + const url = req.query.featureServiceUrl as string + if (!URL.canParse(url)) { + return res.status(404).send('invalid feature service url') + } + + const clientId = req.query.clientId as string + if (!clientId) { + return res.status(404).send('clientId is required') + } + + const config = await processor.safeGetConfig() ArcGISIdentityManager.authorize({ clientId, - portal, - redirectUri - }, res); + portal: getPortalUrl(url), + redirectUri: `${config.baseUrl}/${pluginWebRoute}/oauth/authenticate`, + state: JSON.stringify({ url: url, clientId: clientId }) + }, res) }) + routes.get('/oauth/authenticate', async (req, res) => { - const code = req.query.code as string; - // TODO: Use req or session data to find correct feature service instead of hard coding - // TODO: error handling - const config = await processor.safeGetConfig(); - const featureService = config.featureServices[0]; + const code = req.query.code as string + // TODO is clientId here in req or response + let state: { url: string, clientId: string } + try { + const { url, clientId } = JSON.parse(req.query.state as string) + state = { url, clientId } + } catch (err) { + console.error('error parsing relay state', err) + return res.sendStatus(500) + } + + const config = await processor.safeGetConfig() const creds = { - clientId: (featureService.auth as OAuthAuthConfig)?.clientId as string, - redirectUri: (featureService.auth as OAuthAuthConfig)?.redirectUri as string, - portal: featureService.url as string + clientId: state.clientId, + redirectUri: `${config.baseUrl}/${pluginWebRoute}/oauth/authenticate`, + portal: getPortalUrl(state.url) } - ArcGISIdentityManager.exchangeAuthorizationCode(creds, code) - .then(async (idManager: ArcGISIdentityManager) => { - featureService.auth = { - ...featureService.auth, - authToken: idManager.token, - authTokenExpires: idManager.tokenExpires.toISOString(), - refreshToken: idManager.refreshToken, - refreshTokenExpires: idManager.refreshTokenExpires.toISOString(), - type: AuthType.OAuth, - clientId: creds.clientId - } - await processor.putConfig(config); - res.status(200).json({}) + ArcGISIdentityManager.exchangeAuthorizationCode(creds, code).then(async (idManager: ArcGISIdentityManager) => { + let service = config.featureServices.find(service => service.url === state.url) + if (!service) { + service = { url: state.url, layers: [] } + config.featureServices.push(service) + } + + service.auth = { + ...service.auth, + type: AuthType.OAuth, + clientId: state.clientId, + authToken: idManager.token, + authTokenExpires: idManager.tokenExpires.getTime(), + refreshToken: idManager.refreshToken, + refreshTokenExpires: idManager.refreshTokenExpires.getTime() + } + + await processor.putConfig(config) + + res.send(` + + + + + + `); }).catch((error) => res.status(400).json(error)) }) + return routes }, protected: (requestContext: GetAppRequestContext) => { @@ -233,10 +146,11 @@ const arcgisPluginHooks: InitPluginHook = { } next() }) + routes.route('/config') .get(async (req, res, next) => { console.info('Getting ArcGIS plugin config...') - const config = await processor.safeGetConfig(); + const config = await processor.safeGetConfig() res.json(config) }) .put(async (req, res, next) => { @@ -245,38 +159,62 @@ const arcgisPluginHooks: InitPluginHook = { const configString = JSON.stringify(arcConfig) console.info(configString) processor.putConfig(arcConfig) - res.status(200).json({}) // TODO: Why returning 200 with an empty object here, should we update? + res.sendStatus(200) }) - routes.route('/arcgisLayers') - .get(async (req, res, next) => { - const featureUrl = req.query.featureUrl as string; - console.info('Getting ArcGIS layer info for ' + featureUrl) - let identityManager: ArcGISIdentityManager; - const httpClient = new HttpClient(console); - try { - identityManager = await handleAuthentication(req, httpClient, processor); + routes.post('/featureService/validate', async (req, res) => { + const config = await processor.safeGetConfig() + const { url, auth = {} } = req.body + const { token, username, password } = auth + if (!URL.canParse(url)) { + return res.send('Invalid feature service url').status(400) + } + + let service: FeatureServiceConfig + if (token) { + service = { url, layers: [], auth: { type: AuthType.Token, token } } + } else if (username && password) { + service = { url, layers: [], auth: { type: AuthType.UsernamePassword, username, password } } + } else { + return res.sendStatus(400) + } - const featureUrlAndToken = featureUrl + '?token=' + encodeURIComponent(identityManager.token); - console.log('featureUrlAndToken', featureUrlAndToken); - - httpClient.sendGetHandleResponse(featureUrlAndToken, (chunk) => { - console.info('ArcGIS layer info response ' + chunk); - try { - const featureServiceResult = JSON.parse(chunk) as FeatureServiceResult; - res.json(featureServiceResult); - } catch (e) { - if (e instanceof SyntaxError) { - console.error('Problem with url response for url ' + featureUrl + ' error ' + e) - res.status(200).json({}) // TODO: Why returning 200 with an empty object here, should we update? - } else { - throw e; - } - } - }); - } catch (err) { - res.status(500).json({ message: 'Could not get ArcGIS layer info', error: err }); + try { + const httpClient = new HttpClient(console) + await getIdentityManager(service!, httpClient) + let existingService = config.featureServices.find(service => service.url === url) + if (existingService) { + existingService = { ...existingService } + } else { + config.featureServices.push(service) } + + await processor.putConfig(config) + return res.send(service) + } catch (err) { + return res.send('Invalid username/password').status(400) + } + }) + + routes.get('/featureService/layers', async (req, res, next) => { + const url = req.query.featureServiceUrl as string + const config = await processor.safeGetConfig() + const featureService = config.featureServices.find(featureService => featureService.url === url) + if (!featureService) { + return res.status(400) + } + + const httpClient = new HttpClient(console) + try { + const identityManager = await getIdentityManager(featureService, httpClient) + const response = await request(url, { + authentication: identityManager + }) + res.send(response.layers) + } catch (err) { + console.error(err) + res.status(500).json({ message: 'Could not get ArcGIS layer info', error: err }) + } }) return routes diff --git a/plugins/arcgis/web-app/projects/main/src/lib/ArcGISPluginConfig.ts b/plugins/arcgis/web-app/projects/main/src/lib/ArcGISPluginConfig.ts index 2d5592c20..831ce8b9a 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/ArcGISPluginConfig.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/ArcGISPluginConfig.ts @@ -10,6 +10,11 @@ export interface ArcGISPluginConfig { */ enabled: boolean + /** + * Mage base server url + */ + baseUrl: string + /** * Query the database for new observations to process at the given * repeating time interval in seconds. @@ -128,7 +133,8 @@ export interface ArcGISPluginConfig { } export const defaultArcGISPluginConfig = Object.freeze({ - enabled: true, + enabled: false, + baseUrl: '', intervalSeconds: 60, startupIntervalSeconds: 1, updateIntervalSeconds: 1, diff --git a/plugins/arcgis/web-app/projects/main/src/lib/EventsResult.ts b/plugins/arcgis/web-app/projects/main/src/lib/EventsResult.ts deleted file mode 100644 index 91ac9e074..000000000 --- a/plugins/arcgis/web-app/projects/main/src/lib/EventsResult.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class EventResult { - name: string; - id: number; - forms: FormResult[]; -} - -export class FormResult { - name: string; - id: number; - fields: FieldResult[]; -} - -export class FieldResult { - title: string; -} diff --git a/plugins/arcgis/web-app/projects/main/src/lib/FeatureServiceResult.ts b/plugins/arcgis/web-app/projects/main/src/lib/FeatureServiceResult.ts deleted file mode 100644 index aaab026ce..000000000 --- a/plugins/arcgis/web-app/projects/main/src/lib/FeatureServiceResult.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * ArcGIS feature service result. - * https://developers.arcgis.com/rest/services-reference/enterprise/feature-service.htm - */ -export interface FeatureServiceResult { - - /** - * The layers. - */ - layers: FeatureLayer[] - -} - -/** - * ArcGIS feature service layer. - */ -export interface FeatureLayer { - - /** - * The layer id. - */ - id: number - - /** - * The layer name. - */ - name: string - - /** - * The geometry type. - */ - geometryType: string - -} \ No newline at end of file diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-admin/arc-admin.component.html b/plugins/arcgis/web-app/projects/main/src/lib/arc-admin/arc-admin.component.html index e9527656d..b0a5a6cba 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/arc-admin/arc-admin.component.html +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-admin/arc-admin.component.html @@ -16,6 +16,14 @@

Processing + + {{config.baseUrl}} + Interval (s)

info_outline +
+ + + + +
Interval diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-admin/arc-admin.component.ts b/plugins/arcgis/web-app/projects/main/src/lib/arc-admin/arc-admin.component.ts index 914ba8304..887a9e077 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/arc-admin/arc-admin.component.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-admin/arc-admin.component.ts @@ -2,9 +2,8 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog' import { AttributeConfig, AttributeConcatenationConfig, AttributeDefaultConfig, AttributeValueConfig } from '../ArcGISConfig'; import { ArcGISPluginConfig, defaultArcGISPluginConfig } from '../ArcGISPluginConfig' -import { ArcService } from '../arc.service' +import { ArcService, Form, MageEvent } from '../arc.service' import { Subject } from 'rxjs'; -import { EventResult, FormResult } from '../EventsResult'; @Component({ selector: 'arc-admin', @@ -26,7 +25,7 @@ export class ArcAdminComponent implements OnInit { editName: string; editValue: any; editOptions: any[]; - events: EventResult[] = []; + events: MageEvent[] = []; @ViewChild('infoDialog', { static: true }) private infoTemplate: TemplateRef @@ -59,6 +58,9 @@ export class ArcAdminComponent implements OnInit { this.editFieldMappings = false; arcService.fetchArcConfig().subscribe(x => { this.config = x; + if (!this.config.baseUrl) { + this.config.baseUrl = window.location.origin + } arcService.fetchPopulatedEvents().subscribe(x => this.handleEventResults(x)); }) } @@ -71,7 +73,7 @@ export class ArcAdminComponent implements OnInit { ngOnInit(): void { } - handleEventResults(x: EventResult[]) { + handleEventResults(x: MageEvent[]) { this.events = x } @@ -311,7 +313,7 @@ export class ArcAdminComponent implements OnInit { return omit } - findEvent(event: string): EventResult | undefined { + findEvent(event: string): MageEvent | undefined { let eventResult = undefined if (this.events != undefined) { const index = this.events.findIndex((element) => { @@ -324,7 +326,7 @@ export class ArcAdminComponent implements OnInit { return eventResult } - findForm(event: string, form: string): FormResult | undefined { + findForm(event: string, form: string): Form | undefined { let formResult = undefined let eventResult = this.findEvent(event) if (eventResult != undefined && eventResult.forms != undefined) { diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-event/arc-event.component.ts b/plugins/arcgis/web-app/projects/main/src/lib/arc-event/arc-event.component.ts index 8a7baf53b..3efbe07a6 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/arc-event/arc-event.component.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-event/arc-event.component.ts @@ -1,14 +1,12 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, TemplateRef, ViewChild } from '@angular/core'; import { ArcGISPluginConfig, defaultArcGISPluginConfig } from '../ArcGISPluginConfig' import { FeatureServiceConfig } from "../ArcGISConfig" -import { ArcService } from '../arc.service' +import { ArcService, MageEvent } from '../arc.service' import { MatDialog } from '@angular/material/dialog' import { ArcEventsModel } from './ArcEventsModel'; import { ArcEvent } from './ArcEvent'; import { ArcEventLayer } from './ArcEventLayer'; import { Observable, Subscription } from 'rxjs'; -import { EventResult } from '../EventsResult'; - @Component({ selector: 'arc-event', @@ -53,10 +51,10 @@ export class ArcEventComponent implements OnInit, OnChanges { } handleConfigChanged() { - let eventResults = new Array(); + let eventResults = new Array(); if (this.model.events.length > 0) { for (const arcEvent of this.model.events) { - const result = new EventResult(); + const result = new MageEvent(); result.name = arcEvent.name; result.id = arcEvent.id; eventResults.push(result); @@ -67,7 +65,7 @@ export class ArcEventComponent implements OnInit, OnChanges { } } - handleEventResults(x: EventResult[]) { + handleEventResults(x: MageEvent[]) { let activeEventMessage = 'Active events: '; this.updateLayersCount(); for (const event of x) { diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.html b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.html new file mode 100644 index 000000000..a1f447ef7 --- /dev/null +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.html @@ -0,0 +1,9 @@ +

Delete Feature Service?

+ +{{url}} + + + + + \ No newline at end of file diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.scss b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.spec.ts b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.spec.ts new file mode 100644 index 000000000..826571959 --- /dev/null +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { ArcLayerDeleteDialogComponent } from './arc-layer-delete-dialog.component'; + +describe('Arc Layer Delete Dialog', () => { + let component: ArcLayerDeleteDialogComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ArcLayerDeleteDialogComponent], + imports: [HttpClientTestingModule], + providers: [{ + provide: MatDialogRef, + useValue: {} + },] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ArcLayerDeleteDialogComponent); + component = fixture.componentInstance; + }); + + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.ts b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.ts new file mode 100644 index 000000000..4b80bc69c --- /dev/null +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-delete-dialog.component.ts @@ -0,0 +1,19 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'arc-layer-delete-dialog', + templateUrl: 'arc-layer-delete-dialog.component.html', + styleUrls: ['./arc-layer-delete-dialog.component.scss'] +}) +export class ArcLayerDeleteDialogComponent { + + url: string + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: string + ) { + this.url = data + } +} diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.html b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.html new file mode 100644 index 000000000..5644e38b9 --- /dev/null +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.html @@ -0,0 +1,97 @@ +
+
ArcGIS Feature Service
+ + +
+ + + + Feature Service + + +
+ + URL + + URL is required + + + + Authentication + + + {{authenticationType.title}} + + + + + +
+ + Token + + Token is required + +
+
+ + Client Id + + Client Id is required + +
+
+ + Username + + Username Id is required + + + + Password + + Password is required + +
+
+
+ +
+ +
+
+ + + + Layers + + +
+
+ + + {{layer.name}} + + +
+ +
+ +
+
+
+
+
+
+
\ No newline at end of file diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.scss b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.scss new file mode 100644 index 000000000..a0671015d --- /dev/null +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.scss @@ -0,0 +1,39 @@ +mat-form-field { + width: 100%; +} + +mat-dialog-content { + flex: 1 +} + +.dialog { + min-width: 700px; + min-height: 450px; + position: relative; +} + +.dialog-content { + margin: 16px 0; +} + +.form { + font-size: 16px; +} + +.layers { + margin-bottom: 16px; +} + +.actions { + width: 100%; + display: flex; + flex-direction: column; + align-items: end; +} + +.actions__save { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} \ No newline at end of file diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.spec.ts b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.spec.ts new file mode 100644 index 000000000..79b6b67a4 --- /dev/null +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { ArcLayerDialogComponent } from './arc-layer-dialog.component'; + +describe('Arc Layer Dialog', () => { + let component: ArcLayerDialogComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ArcLayerDialogComponent], + imports: [HttpClientTestingModule], + providers: [{ + provide: MatDialogRef, + useValue: {} + },] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ArcLayerDialogComponent); + component = fixture.componentInstance; + }); + + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.ts b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.ts new file mode 100644 index 000000000..fefd91061 --- /dev/null +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer-dialog.component.ts @@ -0,0 +1,137 @@ +import { Component, Inject, ViewChild } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatSelectionList } from '@angular/material/list'; +import { AuthType, FeatureServiceConfig } from '../ArcGISConfig'; +import { ArcService, FeatureLayer } from '../arc.service'; + +enum State { Validate, Layers } + +interface AuthenticationType { + title: string + value: string +} + +export interface DialogData { + featureService?: FeatureServiceConfig +} + +@Component({ + selector: 'arc-layer-dialog', + templateUrl: 'arc-layer-dialog.component.html', + styleUrls: ['./arc-layer-dialog.component.scss'] +}) +export class ArcLayerDialogComponent { + State = State + state: State = State.Validate + + loading = false + + AuthenticationType = AuthType + authenticationTypes: AuthenticationType[] = [{ + title: 'OAuth', + value: AuthType.OAuth + },{ + title: 'Username/Password', + value: AuthType.UsernamePassword + },{ + title: 'Token', + value: AuthType.Token + }] + + layerForm: FormGroup + layers: FeatureLayer[] + featureService: FeatureServiceConfig + + @ViewChild(MatSelectionList) layerList: MatSelectionList + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData, + private arcService: ArcService + ) { + if (data.featureService) { + this.featureService = data.featureService + } + const auth: any = this.featureService?.auth || {} + const { type, token, username, password, clientId } = auth + + this.state = this.featureService === undefined ? State.Validate : State.Layers + // TODO update all fields with info from pass in service + this.layerForm = new FormGroup({ + url: new FormControl(this.featureService?.url, [Validators.required]), + authenticationType: new FormControl(type || AuthType.OAuth, [Validators.required]), + token: new FormGroup({ + token: new FormControl(token, [Validators.required]) + }), + oauth: new FormGroup({ + clientId: new FormControl(clientId, [Validators.required]) + }), + local: new FormGroup({ + username: new FormControl(username, [Validators.required]), + password: new FormControl(password, [Validators.required]) + }) + }) + + if (this.featureService) { + this.fetchLayers(this.featureService.url) + } + } + + hasLayer(featureLayer: FeatureLayer): boolean { + return this.featureService.layers.some(layer => layer.layer === featureLayer.name) + } + + fetchLayers(url: string): void { + this.loading = true + this.arcService.fetchFeatureServiceLayers(url).subscribe(layers => { + this.layers = layers + this.loading = false + }) + } + + onPanelOpened(state: State): void { + this.state = state + } + + onValidate(): void { + this.loading = true + const { url, authenticationType } = this.layerForm.value + + switch (authenticationType) { + case AuthType.Token: { + const { token } = this.layerForm.controls.token.value + this.featureService = { url, auth: { type: AuthType.Token, token }, layers: [] } + this.arcService.validateFeatureService(this.featureService).subscribe(() => this.validated(url)) + + break; + } + case AuthType.OAuth: { + const { clientId } = this.layerForm.controls.oauth.value + this.featureService = { url, auth: { type: AuthType.OAuth, clientId }, layers: [] } + this.arcService.oauth(url, clientId).subscribe(() => this.validated(url)) + + break; + } + case AuthType.UsernamePassword: { + const { username, password } = this.layerForm.controls.local.value + this.featureService = { url, auth: { type: AuthType.UsernamePassword, username, password }, layers: [] } + this.arcService.validateFeatureService(this.featureService).subscribe(() => this.validated(url)) + + break; + } + } + } + + validated(url: string): void { + this.fetchLayers(url) + this.state = State.Layers + } + + onSave(): void { + this.featureService.layers = this.layerList.selectedOptions.selected.map(option => { + return { layer: `${option.value}` } + }) + this.dialogRef.close(this.featureService) + } +} diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.html b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.html index 068a060b9..931d1c6e6 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.html +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.html @@ -5,86 +5,33 @@

Feature Layers

Add ArcGIS feature service urls and layers to sychronize MAGE data to.

- -
-
- +
There are no ArcGIS feature services currently being synchronized.
-
- -
- -
- -
-
- -
+ +
+
+ -
-
- {{featureLayer.layer}} -
+
+ +
+
+ +
+
+
+
+ {{layer.layer}}
- -
- -
- - -

Add ArcGIS Feature Service

-
- -

Edit ArcGIS Feature Service

-
- -
- - URL - - - URL is required - - - - Token - - -
-

Layers

- -
-
- - {{arcLayer.name}}
-
- - - - -
- -

Delete feature service?

-
- {{currentUrl}} -
-
- - - - -
\ No newline at end of file + +
\ No newline at end of file diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts index b96b4051f..650e34b54 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts @@ -1,310 +1,101 @@ -import { Component, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild } from '@angular/core'; -import { FormControl, Validators } from '@angular/forms' +import { Component, EventEmitter, Input, Output } from '@angular/core' import { ArcGISPluginConfig, defaultArcGISPluginConfig } from '../ArcGISPluginConfig' import { ArcService } from '../arc.service' -import { AuthType, OAuthAuthConfig, TokenAuthConfig, UsernamePasswordAuthConfig, FeatureServiceConfig } from '../ArcGISConfig'; +import { FeatureLayerConfig, FeatureServiceConfig } from '../ArcGISConfig' import { MatDialog } from '@angular/material/dialog' -import { ArcLayerSelectable } from './ArcLayerSelectable'; -import { EventResult } from '../EventsResult'; +import { ArcLayerSelectable } from './ArcLayerSelectable' +import { ArcLayerDialogComponent, DialogData } from './arc-layer-dialog.component' +import { ArcLayerDeleteDialogComponent } from './arc-layer-delete-dialog.component' @Component({ selector: 'arc-layer', templateUrl: './arc-layer.component.html', styleUrls: ['./arc-layer.component.scss'] }) -export class ArcLayerComponent implements OnInit { +export class ArcLayerComponent { - @Input('config') config: ArcGISPluginConfig; - @Output() configChanged = new EventEmitter(); + @Input('config') config: ArcGISPluginConfig + @Output() configChanged = new EventEmitter() - layers: ArcLayerSelectable[]; - events: string[] = []; - arcLayerControl = new FormControl('', [Validators.required]) - arcTokenControl = new FormControl('') - isLoading: boolean; - currentUrl?: string; - private timeoutId: number; - - @ViewChild('addLayerDialog', { static: true }) - private addLayerTemplate: TemplateRef - @ViewChild('deleteLayerDialog', { static: true }) - private deleteLayerTemplate: TemplateRef + layers: ArcLayerSelectable[] + events: string[] = [] constructor(private arcService: ArcService, private dialog: MatDialog) { - this.config = defaultArcGISPluginConfig; - this.layers = new Array(); - this.isLoading = false; - arcService.fetchEvents().subscribe(x => this.handleEventResults(x)); - } - - ngOnInit(): void { - - } - - handleEventResults(x: EventResult[]) { - const events = [] - for (const event of x) { - events.push(event.name) - } - this.events = events - } - - onEditLayer(arcService: FeatureServiceConfig) { - console.log('Editing layer ' + arcService.url) - if (arcService.auth?.type === AuthType.Token && arcService.auth?.token) { - this.currentUrl = this.addToken(arcService.url, arcService.auth.token); - } else if (arcService.auth?.type === AuthType.UsernamePassword && arcService.auth?.username && arcService.auth?.password) { - this.currentUrl = this.addCredentials(arcService.url, arcService.auth?.username, arcService.auth?.password); - } else if (arcService.auth?.type === AuthType.OAuth && arcService.auth?.clientId) { - // TODO: what needs to be sent in url for this to work? - this.currentUrl = this.addOAuth(arcService.url, arcService.auth?.clientId); - } else { - throw new Error('Invalid layer config, auth credentials: ' + JSON.stringify(arcService)) - } - - this.arcLayerControl.setValue(arcService.url) - // Safely set the token control value based on the type - if (arcService.auth?.type === AuthType.Token) { - this.arcTokenControl.setValue(arcService.auth.token); - } else { - this.arcTokenControl.setValue(''); - } - this.layers = [] - let selectedLayers = new Array() - for (const layer of arcService.layers) { - selectedLayers.push(String(layer.layer)) - } - this.fetchLayers(this.currentUrl, selectedLayers) - this.dialog.open(this.addLayerTemplate) - } + this.config = defaultArcGISPluginConfig + this.layers = new Array() - selectedChanged(arcLayer: ArcLayerSelectable) { - arcLayer.isSelected = !arcLayer.isSelected - } - - isSaveDisabled(): boolean { - return this.layers.length == 0; - } - - inputChanged(layerUrl: string, token?: string, username?: string, password?: string) { - let url: string; - //TODO - switch to username/pw being in body and avoid in url query - //TODO - remove hardcoded username/pw and update UI to provide - username = 'username_example' - password = 'password_example' - - if (token) { - url = this.addToken(layerUrl, token); - } else if (username && password) { - url = this.addCredentials(layerUrl, username, password); - } else { - url = layerUrl; - } - //TODO - avoid logging plain text password - console.log('Input changed ' + url); - if (this.timeoutId !== undefined) { - window.clearTimeout(this.timeoutId); - } - this.timeoutId = window.setTimeout(() => this.fetchLayers(url), 1000); + arcService.fetchEvents().subscribe(events => { + this.events = events.map(event => event.name) + }) } - fetchLayers(url: string, selectedLayers?: string[]) { - console.log('Fetching layers for ' + url); - this.isLoading = true; - this.layers = [] - this.arcService.fetchArcLayers(url).subscribe(x => { - console.log('arclayer response ' + x); - if (x.layers !== undefined) { - for (const layer of x.layers) { - const selectableLayer = new ArcLayerSelectable(layer.name); - if (selectedLayers != null) { - if (selectedLayers.length > 0) { - selectableLayer.isSelected = selectedLayers.indexOf(layer.name) >= 0; - } else { - selectableLayer.isSelected = false - } - } - this.layers.push(selectableLayer); - } + onAddService() { + this.dialog.open(ArcLayerDialogComponent, { + data: { + featureService: undefined + }, + autoFocus: false + }).afterClosed().subscribe(featureService => { + if (featureService) { + this.addFeatureService(featureService) } - this.isLoading = false; }) } - onSignIn() { - this.arcService.authenticate().subscribe({ - next(x) { - console.log('got value ' + JSON.stringify(x)); + onEditService(featureService: FeatureServiceConfig) { + this.dialog.open(ArcLayerDialogComponent, { + data: { + featureService: featureService }, - error(err) { - console.error('something wrong occurred: ' + err); - }, - complete() { - console.log('done'); - }, - }); - } - - onAddLayer() { - this.currentUrl = undefined - this.arcLayerControl.setValue('') - this.arcTokenControl.setValue('') - this.layers = [] - this.dialog.open(this.addLayerTemplate) - } - - showDeleteLayer(layerUrl: string) { - this.currentUrl = layerUrl - this.dialog.open(this.deleteLayerTemplate) - } - - onDeleteLayer() { - let index = 0; - for (const featureServiceConfig of this.config.featureServices) { - if (featureServiceConfig.url == this.currentUrl) { - break; + autoFocus: false + }).afterClosed().subscribe(featureService => { + if (featureService) { + this.addFeatureService(featureService) } - index++; - } - if (index < this.config.featureServices.length) { - this.config.featureServices.splice(index, 1); - } - this.configChanged.emit(this.config); - this.arcService.putArcConfig(this.config); + }) } - /** - * Adds or edits a layer configuration based on the provided parameters. - * - * @param params - The parameters for adding or editing a layer. - * @param params.layerUrl - The URL of the layer to add or edit. - * @param params.selectableLayers - An array of selectable layers. - * @param params.authType - The type of authentication to use. - * @param params.layerToken - Optional. The token for token-based authentication. - * @param params.username - Optional. The username for username/password authentication. - * @param params.password - Optional. The password for username/password authentication. - * @param params.clientId - Optional. The client ID for OAuth authentication. - * @param params.clientSecret - Optional. The client secret for OAuth authentication. - * - * @throws Will throw an error if the provided authentication type is invalid. - */ - onAddLayerUrl(params: { - layerUrl: string, - selectableLayers: ArcLayerSelectable[], - authType: AuthType, - layerToken?: string, - username?: string, - password?: string, - clientId?: string, - clientSecret?: string - }): void { - let serviceConfigToEdit = null; - const { layerUrl, selectableLayers, authType, layerToken, username, password, clientId, clientSecret } = params; - - // Search if the layer in config to edit - for (const service of this.config.featureServices) { - if (service.url == layerUrl) { - serviceConfigToEdit = service; - } - } - - // Add layer if it doesn't exist - if (serviceConfigToEdit == null) { - console.log('Adding layer ' + layerUrl); - - const authConfigMap = { - [AuthType.Token]: { type: AuthType.Token, token: layerToken } as TokenAuthConfig, - [AuthType.UsernamePassword]: { type: AuthType.UsernamePassword, username, password } as UsernamePasswordAuthConfig, - [AuthType.OAuth]: { type: AuthType.OAuth, clientId, clientSecret } as OAuthAuthConfig - }; + onDeleteService(featureService: FeatureServiceConfig) { + this.dialog.open(ArcLayerDeleteDialogComponent, { + data: featureService.url + }).afterClosed().subscribe(result => { + if (result === true) { + this.config.featureServices = this.config.featureServices.filter(service => { + service.url !== featureService.url + }) - const authConfig = authConfigMap[authType]; - if (!authConfig) { - throw new Error('Invalid authorization type: ' + authType); + this.configChanged.emit(this.config) + this.arcService.putArcConfig(this.config) } + }) + } - // Creates a new feature layer configuration. - const featureLayer: FeatureServiceConfig = { - url: layerUrl, - auth: authConfig, - layers: [] - } as FeatureServiceConfig; + addFeatureService(featureServer: FeatureServiceConfig): void { + const existingFeatureServer = this.config.featureServices.find((service) => { + return service.url === featureServer.url + }) - // Adds selected layers to the feature layer configuration. - if (selectableLayers) { - for (const aLayer of selectableLayers) { - if (aLayer.isSelected) { - const layerConfig = { - layer: aLayer.name, - events: JSON.parse(JSON.stringify(this.events)) - } - featureLayer.layers.push(layerConfig); - } + if (existingFeatureServer == null) { + featureServer.layers = featureServer.layers.map((layer: FeatureLayerConfig) => { + return { + ...layer, + events: JSON.parse(JSON.stringify(this.events)) } - - // Add the new featureLayer to the config and emits the change. - this.config.featureServices.push(featureLayer); - this.configChanged.emit(this.config); - // Persists the updated configuration using `arcService`. - this.arcService.putArcConfig(this.config); - } - - } else { // Edit existing layer - console.log('Saving edited layer ' + layerUrl) - const editedLayers = []; - if (selectableLayers) { - for (const aLayer of selectableLayers) { - if (aLayer.isSelected) { - let layerConfig = null - if (serviceConfigToEdit.layers != null) { - const index = serviceConfigToEdit.layers.findIndex((element) => { - return element.layer === aLayer.name; - }) - if (index != -1) { - layerConfig = serviceConfigToEdit.layers[index] - } - } - if (layerConfig == null) { - layerConfig = { - layer: aLayer.name, - events: JSON.parse(JSON.stringify(this.events)) - } - } - editedLayers.push(layerConfig); - } + }) + + this.config.featureServices.push(featureServer) + } else { + existingFeatureServer.layers = featureServer.layers.map(layer => { + const existing = existingFeatureServer.layers.some(edit => edit.layer === layer.layer) + if (!existing) { + layer.events = JSON.parse(JSON.stringify(this.events)) } - } - serviceConfigToEdit.layers = editedLayers - } - // Emit and persist the updated configuration. - this.configChanged.emit(this.config); - this.arcService.putArcConfig(this.config); - } - // Provide html access to auth types - public AuthType = AuthType; - // Helper method to add token to the URL - // append to layerURL query parameter, outer url already contains ? separator - addToken(url: string, token?: string) { - let newUrl = url - if (token != null && token.length > 0) { - newUrl += '&' + 'token=' + encodeURIComponent(token) + return layer + }) } - return newUrl - } - - // Helper method to add credentials to the URL - // append to layerURL query parameter, outer url already contains ? separator - private addCredentials(layerUrl: string, username: string, password: string): string { - const encodedUsername = encodeURIComponent(username); - const encodedPassword = encodeURIComponent(password); - return `${layerUrl}&username=${encodedUsername}&password=${encodedPassword}`; - } - // Helper method to add OAuth credentials to the URL - // append to layerURL query parameter, outer url already contains ? separator - private addOAuth(layerUrl: string, clientId: string): string { - const encodedClientId = encodeURIComponent(clientId); - return `${layerUrl}&client_id=${encodedClientId}`; + this.configChanged.emit(this.config) + this.arcService.putArcConfig(this.config) } } \ No newline at end of file diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc.service.ts b/plugins/arcgis/web-app/projects/main/src/lib/arc.service.ts index a4170c753..79cf6d7c2 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/arc.service.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc.service.ts @@ -2,84 +2,96 @@ import { HttpClient } from '@angular/common/http' import { Injectable } from '@angular/core' import { Observable, Subject } from 'rxjs' import { ArcGISPluginConfig } from './ArcGISPluginConfig' -import { FeatureServiceResult } from './FeatureServiceResult' -import { EventResult } from './EventsResult' +import { FeatureServiceConfig } from './ArcGISConfig' export const baseUrl = '/plugins/@ngageoint/mage.arcgis.service' export const apiBaseUrl = '/api' export interface ArcServiceInterface { - fetchArcConfig(): Observable; - fetchArcLayers(featureUrl: string): Observable; - fetchEvents(): Observable; - fetchPopulatedEvents(): Observable; - putArcConfig(config: ArcGISPluginConfig): void; - removeUserTrack(userTrackId: string): Observable; - removeOperation(operationId: string): Observable; + fetchArcConfig(): Observable + putArcConfig(config: ArcGISPluginConfig): void + fetchEvents(): Observable + fetchPopulatedEvents(): Observable + fetchFeatureServiceLayers(featureServiceUrl: string): Observable +} + +export class MageEvent { + name: string + id: number + forms: Form[] +} + +export class Form { + name: string + id: number + fields: Field[] +} + +export class Field { + title: string +} + +export interface FeatureLayer { + id: number + name: string + geometryType: string } @Injectable({ providedIn: 'root' - /* - TODO: figure out how to inject the same http client the - rest of the core app gets so the http auth interceptor - applies when this service comes from a non-root module - providedIn: MageArcServicesModule - */ }) export class ArcService implements ArcServiceInterface { - constructor(private http: HttpClient) { - } + constructor( + private http: HttpClient + ) {} fetchArcConfig(): Observable { return this.http.get(`${baseUrl}/config`) } - fetchArcLayers(featureUrl: string) { - return this.http.get(`${baseUrl}/arcgisLayers?featureUrl=${featureUrl}`) + fetchFeatureServiceLayers(featureServiceUrl: string) { + return this.http.get(`${baseUrl}/featureService/layers?featureServiceUrl=${encodeURIComponent(featureServiceUrl)}`) } - authenticate(): Observable { + oauth(featureServiceUrl: string, clientId: string): Observable { let subject = new Subject(); - const url = `${baseUrl}/oauth/sign-in`; - const authWindow = window.open(url, "_blank"); + const url = `${baseUrl}/oauth/signin?featureServiceUrl=${encodeURIComponent(featureServiceUrl)}&clientId=${encodeURIComponent(clientId)}`; + const oauthWindow = window.open(url, "_blank"); - function onMessage(event: any) { - window.removeEventListener('message', onMessage, false); + const listener = (event: any) => { + console.log('got window message', event) + + window.removeEventListener('message', listener, false); if (event.origin !== window.location.origin) { - return; + subject.error('target origin mismatch') } subject.next(event.data) - // TODO: Fix window to send data - // authWindow?.close(); + + oauthWindow?.close(); } - authWindow?.addEventListener('message', onMessage, false); + window.addEventListener('message', listener, false); return subject.asObservable() } - fetchEvents() { - return this.http.get(`${apiBaseUrl}/events?populate=false&projection={"name":true,"id":true}`) + validateFeatureService(service: FeatureServiceConfig): Observable { + return this.http.post(`${baseUrl}/featureService/validate`, service) + } + + fetchEvents(): Observable { + return this.http.get(`${apiBaseUrl}/events?populate=false&projection={"name":true,"id":true}`) } fetchPopulatedEvents() { - return this.http.get(`${apiBaseUrl}/events`) + return this.http.get(`${apiBaseUrl}/events`) } putArcConfig(config: ArcGISPluginConfig) { this.http.put(`${baseUrl}/config`, config).subscribe() } - - removeUserTrack(userTrackId: string): Observable { - return this.http.delete(`${baseUrl}/config/user_tracks/${userTrackId}`) - } - - removeOperation(operationId: string): Observable { - throw new Error('unimplemented') - } } \ No newline at end of file diff --git a/plugins/arcgis/web-app/projects/main/src/lib/mage-arc.module.spec.ts b/plugins/arcgis/web-app/projects/main/src/lib/mage-arc.module.spec.ts index e8d4a6b44..bfadc5d07 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/mage-arc.module.spec.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/mage-arc.module.spec.ts @@ -1,14 +1,11 @@ import { TestBed, waitForAsync } from '@angular/core/testing' -import { ArcAdminComponent, MageArcModule } from '@@main' -import { createComponent, createNgModule, Injector, NgModuleRef, PLATFORM_ID, Type } from '@angular/core' -import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing' -import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations' -import { MatCommonModule } from '@angular/material/core' -import { Platform, PlatformModule } from '@angular/cdk/platform' -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing' -import { A11yModule, HighContrastModeDetector } from '@angular/cdk/a11y' +import { MageArcModule } from '@@main' +import { createNgModule, Injector, NgModuleRef, Type } from '@angular/core' +import { BrowserTestingModule } from '@angular/platform-browser/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { PlatformModule } from '@angular/cdk/platform' +import { A11yModule } from '@angular/cdk/a11y' import { CommonModule } from '@angular/common' -import { BrowserModule } from '@angular/platform-browser' import { ArcService } from './arc.service' import { HttpClientTestingModule } from '@angular/common/http/testing' diff --git a/plugins/arcgis/web-app/projects/main/src/lib/mage-arc.module.ts b/plugins/arcgis/web-app/projects/main/src/lib/mage-arc.module.ts index 28c759447..b924062e1 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/mage-arc.module.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/mage-arc.module.ts @@ -18,12 +18,15 @@ import { MageUserModule } from '@ngageoint/mage.web-core-lib/user' import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ArcLayerComponent } from './arc-layer/arc-layer.component'; import { ArcEventComponent } from './arc-event/arc-event.component'; - +import { ArcLayerDialogComponent } from './arc-layer/arc-layer-dialog.component'; +import { ArcLayerDeleteDialogComponent } from './arc-layer/arc-layer-delete-dialog.component'; @NgModule({ declarations: [ ArcEventComponent, ArcLayerComponent, + ArcLayerDialogComponent, + ArcLayerDeleteDialogComponent, ArcAdminComponent ], imports: [