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
141 changes: 99 additions & 42 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import config from 'shogunApplicationConfig';

import Header from './Component/Header/Header';
import ShogunSpinner from './Component/ShogunSpinner/ShogunSpinner';
import useExecuteWfsDescribeFeatureType, { DescribeFeatureType } from './Hooks/useExecuteWfsDescribeFeatureType';
import useSHOGunAPIClient from './Hooks/useSHOGunAPIClient';
import Portal from './Page/Portal/Portal';
import { appInfoAtom, layerSuggestionListAtom, userInfoAtom } from './State/atoms';
import { appInfoAtom, layerSuggestionListAtom, userInfoAtom, entityIdAtom } from './State/atoms';
import { setSwaggerDocs } from './State/static';

import ProviderResult = languages.ProviderResult;
Expand All @@ -29,6 +30,8 @@ const App: React.FC = () => {
const [, setAppInfo] = useRecoilState(appInfoAtom);
const [layerSuggestionList, setLayerSuggestionList] = useRecoilState(layerSuggestionListAtom);
const [loadingState, setLoadingState] = useState<'failed' | 'loading' | 'done'>();
const [entityId, ] = useRecoilState(entityIdAtom);
const [propertyNames, setPropertyNames] = useState<string[]>([]);

const disposableCompletionItemProviderRef = useRef<IDisposable>();

Expand All @@ -44,6 +47,24 @@ const App: React.FC = () => {
t
} = useTranslation();

useEffect(() => {
const setLayers = async() => {
try {
const layers = await client?.layer().findAll();
if (!_isNil(layers)) {
setLayerSuggestionList(layers.content);
}

if (disposableCompletionItemProviderRef.current) {
disposableCompletionItemProviderRef.current.dispose();
}
} catch (error) {
Logger.error(error);
}
};
setLayers();
}, [client, setLayerSuggestionList]);

const getInitialData = useCallback(async () => {
try {
setLoadingState('loading');
Expand All @@ -66,7 +87,31 @@ const App: React.FC = () => {
}
}, [setAppInfo, setUserInfo, client]);

const registerLayerIdCompletionProvider = useCallback(() => {
const executeWfsDescribeFeatureType = useExecuteWfsDescribeFeatureType();

const getPropertyNames = useCallback(async (layerId: number | undefined) => {
let response: DescribeFeatureType | undefined;
const propNames: string[] = [];
if (layerSuggestionList && layerId) {
const layer = layerSuggestionList.filter(item => item.id === layerId)[0];
if (layer) {
try {
response = await executeWfsDescribeFeatureType(layer);
if (response !== undefined) {
response.featureTypes[0].properties.forEach(prop => {
propNames.push(prop.name);
});
}
} catch (error) {
Logger.error(error);
propNames[0] = '';
}
}
}
return propNames;
}, [executeWfsDescribeFeatureType, layerSuggestionList]);

const registerCompletionProvider = useCallback(() => {
if (!monaco) {
return undefined;
}
Expand All @@ -76,63 +121,75 @@ const App: React.FC = () => {
provideCompletionItems: async (model, position) => {
const lineContent = model.getLineContent(position.lineNumber).trim();

if (!lineContent.startsWith('"layerId"')) {
return null;
if (lineContent.startsWith('"layerId"')) {
const currentWord = model.getWordAtPosition(position);
const providerResult: ProviderResult<CompletionList> = {
suggestions: layerSuggestionList.map((layer): CompletionItem => {
return {
insertText: layer?.id?.toString() ?? '',
label: `${layer.name} (${layer.id})`,
kind: monaco.languages.CompletionItemKind.Value,
documentation: `${JSON.stringify(layer, null, ' ')}`,
range: {
// replace the current word, if applicable
startColumn: currentWord ? currentWord.startColumn : position.column,
endColumn: currentWord ? currentWord.endColumn : position.column,
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
}
};
})
};

return providerResult;
}

if (!layerSuggestionList) {
try {
const layers = await client?.layer().findAll();
if (!_isNil(layers)) {
setLayerSuggestionList(layers.content);
}

if (disposableCompletionItemProviderRef.current) {
disposableCompletionItemProviderRef.current.dispose();
}
} catch (error) {
Logger.error(error);
}
return undefined;
if (lineContent.startsWith('"propertyName"')) {
const currentWord = model.getWordAtPosition(position);
const providerResult: ProviderResult<CompletionList> = {
suggestions: propertyNames.map((prop): CompletionItem => {
return {
insertText: `"${prop}"`,
label: prop,
kind: monaco.languages.CompletionItemKind.Value,
documentation: `${JSON.stringify(prop, null, ' ')}`,
range: {
// replace the current word, if applicable
startColumn: currentWord ? currentWord.startColumn : position.column,
endColumn: currentWord ? currentWord.endColumn : position.column,
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
}
};
})
};
return providerResult;
}

const currentWord = model.getWordAtPosition(position);
const providerResult: ProviderResult<CompletionList> = {
suggestions: layerSuggestionList.map((layer): CompletionItem => {
return {
insertText: layer?.id?.toString() ?? '',
label: `${layer.name} (${layer.id})`,
kind: monaco.languages.CompletionItemKind.Value,
documentation: `${JSON.stringify(layer, null, ' ')}`,
range: {
// replace the current word, if applicable
startColumn: currentWord ? currentWord.startColumn : position.column,
endColumn: currentWord ? currentWord.endColumn : position.column,
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
}
};
})
};

return providerResult;
}
});
}, [monaco, client, setLayerSuggestionList, layerSuggestionList]);
}, [monaco, layerSuggestionList, propertyNames]);

useEffect(() => {
getInitialData();
}, [getInitialData]);

useEffect(() => {
registerLayerIdCompletionProvider();
const propName = async() => {
const properties = await getPropertyNames(entityId);
setPropertyNames(properties);
};
propName();
}, [entityId, getPropertyNames]);

useEffect(() => {
registerCompletionProvider();

return () => {
if (disposableCompletionItemProviderRef.current) {
disposableCompletionItemProviderRef.current.dispose();
}
};
}, [registerLayerIdCompletionProvider]);
}, [registerCompletionProvider]);

if (loadingState === 'loading') {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
useNavigate
} from 'react-router-dom';

import { useSetRecoilState } from 'recoil';
import config from 'shogunApplicationConfig';

import Logger from '@terrestris/base-util/dist/Logger';
Expand All @@ -61,13 +62,15 @@ import {
GenericEntityController
} from '../../../Controller/GenericEntityController';
import useSHOGunAPIClient from '../../../Hooks/useSHOGunAPIClient';
import { entityIdAtom } from '../../../State/atoms';
import TranslationUtil from '../../../Util/TranslationUtil';
import GeneralEntityForm, {
FormConfig
} from '../GeneralEntityForm/GeneralEntityForm';
import GeneralEntityTable, {
TableConfig
} from '../GeneralEntityTable/GeneralEntityTable';

import './GeneralEntityRoot.less';

export interface GeneralEntityConfigType<T extends BaseEntity> {
Expand Down Expand Up @@ -120,6 +123,7 @@ export function GeneralEntityRoot<T extends BaseEntity>({

const location = useLocation();
const navigate = useNavigate();
const setEntityId = useSetRecoilState(entityIdAtom);

const match = matchPath({
path: `${config.appPrefix}/portal/${entityType}/:entityId`
Expand Down Expand Up @@ -314,19 +318,22 @@ export function GeneralEntityRoot<T extends BaseEntity>({
useEffect(() => {
if (!entityId) {
setId(undefined);
setEntityId(undefined);
setEditEntity(undefined);
setFormIsDirty(false);
return;
}
if (entityId === 'create') {
setId(entityId);
setEntityId(undefined);
form.resetFields();
form.setFieldsValue(defaultEntity);
} else {
setId(parseInt(entityId, 10));
setEntityId(parseInt(entityId, 10));
setFormIsDirty(false);
}
}, [entityId, form, defaultEntity]);
}, [entityId, form, defaultEntity, setEntityId]);

// Once the controller is known we need to set the formUpdater so we can update
// a given form when the entity is updated via controller
Expand Down
96 changes: 96 additions & 0 deletions src/Hooks/useExecuteWfsDescribeFeatureType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
useCallback
} from 'react';

import { UrlUtil } from '@terrestris/base-util/dist/UrlUtil/UrlUtil';

import Layer from '@terrestris/shogun-util/dist/model/Layer';
import {
getBearerTokenHeader
} from '@terrestris/shogun-util/dist/security/getBearerTokenHeader';

import useSHOGunAPIClient from './useSHOGunAPIClient';

export type LocalGeometryType = 'MultiPoint' | 'Point' | 'MultiLineString' | 'LineString' | 'MultiPolygon' | 'Polygon';
export type GeometryType = 'gml:MultiPoint' | 'gml:Point' | 'gml:MultiLineString' |
'gml:LineString' | 'gml:MultiPolygon' | 'gml:Polygon';

export interface Property {
localType: 'int' | 'number' | 'string' | 'boolean' | 'date' | LocalGeometryType;
maxOccurs: 0 | 1;
minOccurs: 0 | 1;
name: string;
nillable: boolean;
type: 'xsd:int' | 'xsd:number' | 'xsd:string' | 'xsd:boolean' | 'xsd:date' | GeometryType;
}

export interface FeatureType {
typeName: string;
properties: Property[];
}

export interface DescribeFeatureType {
elementFormDefault: string;
featureTypes: FeatureType[];
targetNamespace: string;
targetPrefix: string;
}

export const isGeometryType = (propertyType: string): propertyType is GeometryType => {
const geometryTypes = [
'gml:MultiPoint',
'gml:Point',
'gml:MultiLineString',
'gml:LineString',
'gml:MultiPolygon',
'gml:Polygon'
];

return geometryTypes.includes(propertyType);
};

export const useExecuteWfsDescribeFeatureType = () => {
const client = useSHOGunAPIClient();

const executeWfsDescribeFeatureType = useCallback(async (layer: Layer) => {
let url = layer.sourceConfig.url;

if (!url) {
return;
}

if (url.endsWith('?')) {
url = url.slice(0, -1);
}

const params = {
SERVICE: 'WFS',
REQUEST: 'DescribeFeatureType',
VERSION: '2.0.0',
OUTPUTFORMAT: 'application/json',
TYPENAMES: layer.sourceConfig.layerNames
};

const defaultHeaders = {
'Content-Type': 'application/json'
};

const response = await fetch(`${url}?${UrlUtil.objectToRequestString(params)}`, {
method: 'GET',
headers: layer.sourceConfig.useBearerToken ? {
...defaultHeaders,
...getBearerTokenHeader(client?.getKeycloak())
} : defaultHeaders
});

if (!response.ok) {
throw new Error('No successful response while executing a WFS DescribeFeatureType');
}

return await response.json() as DescribeFeatureType;
}, [client]);

return executeWfsDescribeFeatureType;
};

export default useExecuteWfsDescribeFeatureType;
5 changes: 5 additions & 0 deletions src/State/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ export const layerSuggestionListAtom = atom<Layer[]>({
key: 'layerSuggestionList',
default: undefined
});

export const entityIdAtom = atom<number | undefined>({
key: 'entityId',
default: undefined
});
Loading