Skip to content

Commit 36d1594

Browse files
authored
Geodashboard skeleton with text widget (#2594)
- Fix #2592 - Fix #2593
1 parent 4567a56 commit 36d1594

103 files changed

Lines changed: 2924 additions & 717 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2017, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
var expect = require('expect');
9+
10+
const {
11+
setEditing, SET_EDITING,
12+
setEditorAvailable, SET_EDITOR_AVAILABLE
13+
} = require('../dashboard');
14+
15+
it('setEditing', () => {
16+
const retval = setEditing();
17+
expect(retval).toExist();
18+
expect(retval.type).toBe(SET_EDITING);
19+
});
20+
it('setEditorAvailable', () => {
21+
const retval = setEditorAvailable();
22+
expect(retval).toExist();
23+
expect(retval.type).toBe(SET_EDITOR_AVAILABLE);
24+
});

web/client/actions/dashboard.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const SET_EDITOR_AVAILABLE = "DASHBOARD:SET_AVAILABLE";
2+
const SET_EDITING = "DASHBOARD:SET_EDITING";
3+
module.exports = {
4+
SET_EDITING,
5+
setEditing: (editing) => ({type: SET_EDITING, editing }),
6+
SET_EDITOR_AVAILABLE,
7+
setEditorAvailable: available => ({type: SET_EDITOR_AVAILABLE, available})
8+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2017, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
const React = require('react');
9+
const { Col, FormGroup, FormControl, Grid, Row } = require('react-bootstrap');
10+
const CatalogServiceSelector = require('./CatalogServiceSelector');
11+
const localizeProps = require('../misc/enhancers/localizedProps');
12+
const SearchInput = localizeProps("placeholder")(FormControl);
13+
module.exports = ({ onSearchTextChange = () => { }, searchText, title, catalog, services, isValidServiceSelected, showCatalogSelector}) =>
14+
( <Grid className="catalog-form" fluid><Row><Col xs={12}>
15+
<h4 className="text-center">{title}</h4>
16+
{showCatalogSelector
17+
? (<FormGroup>
18+
<CatalogServiceSelector servieces={services} catalog={catalog} isValidServiceSelected={isValidServiceSelected}/>
19+
</FormGroup>) : null}
20+
<FormGroup controlId="catalog-form">
21+
<SearchInput type="text" placeholder="catalog.textSearchPlaceholder" value={searchText} onChange={(e) => onSearchTextChange(e.currentTarget.value)}/>
22+
</FormGroup>
23+
</Col></Row></Grid>);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2017, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
const React = require('react');
9+
const {InputGroup, Glyphicon} = require('react-bootstrap');
10+
11+
const localizedProps = require('../misc/enhancers/localizedProps');
12+
const Select = localizedProps(['placeholder', 'clearValueText', 'noResultsText'])(require('react-select'));
13+
14+
module.exports = ({
15+
isValidServiceSelected,
16+
services,
17+
selectedService,
18+
onChangeCatalogMode = () => {},
19+
onChangeSelectedService = () => {}
20+
}) => (<InputGroup>
21+
<Select
22+
clearValueText={"catalog.clearValueText"}
23+
noResultsText={"catalog.noResultsText"}
24+
clearable
25+
options={services}
26+
value={selectedService}
27+
onChange={(val) => onChangeSelectedService(val && val.value ? val.value : "")}
28+
placeholder={"catalog.servicePlaceholder"} />
29+
{isValidServiceSelected ? (<InputGroup.Addon className="btn"
30+
onClick={() => onChangeCatalogMode("edit", false)}>
31+
<Glyphicon glyph="pencil"/>
32+
</InputGroup.Addon>) : null}
33+
<InputGroup.Addon className="btn" onClick={() => onChangeCatalogMode("edit", true)}>
34+
<Glyphicon glyph="plus"/>
35+
</InputGroup.Addon>
36+
</InputGroup>
37+
);
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2017, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
const React = require('react');
9+
const {compose, mapPropsStream} = require('recompose');
10+
const {isNil} = require('lodash');
11+
const Message = require('../I18N/Message');
12+
const Rx = require('rxjs');
13+
14+
const API = {
15+
"csw": require('../../api/CSW'),
16+
"wms": require('../../api/WMS'),
17+
"wmts": require('../../api/WMTS')
18+
};
19+
20+
const BorderLayout = require('../layout/BorderLayout');
21+
const LoadingSpinner = require('../misc/LoadingSpinner');
22+
const withVirtualScroll = require('../misc/enhancers/infiniteScroll/withInfiniteScroll');
23+
const loadingState = require('../misc/enhancers/loadingState');
24+
const emptyState = require('../misc/enhancers/emptyState');
25+
const withControllableState = require('../misc/enhancers/withControllableState');
26+
const CatalogForm = require('./CatalogForm');
27+
const {getCatalogRecords} = require('../../utils/CatalogUtils');
28+
const Icon = require('../misc/FitIcon');
29+
const defaultPreview = <Icon glyph="geoserver" padding={20}/>;
30+
const SideGrid = compose(
31+
loadingState(({loading, items = []} ) => items.length === 0 && loading),
32+
emptyState(
33+
({loading, items = []} ) => items.length === 0 && !loading,
34+
{
35+
title: <Message msgId="catalog.noRecordsMatched" />,
36+
style: { transform: "translateY(50%)"}
37+
})
38+
39+
)(require('../misc/cardgrids/SideGrid'));
40+
/*
41+
* converts record item into a item for SideGrid
42+
*/
43+
const resToProps = ({records, result = {}}) => ({
44+
items: (records || []).map((record = {}) => ({
45+
title: record.title,
46+
caption: record.identifier,
47+
description: record.description,
48+
preview: record.thumbnail ? <img src="thumbnail" /> : defaultPreview,
49+
record
50+
})),
51+
total: result && result.numberOfRecordsMatched
52+
});
53+
const PAGE_SIZE = 10;
54+
/*
55+
* retrieves data from a catalog service and converts to props
56+
*/
57+
const loadPage = ({text, catalog = {}}, page = 0) => Rx.Observable
58+
.fromPromise(API[catalog.type].textSearch(catalog.url, page * PAGE_SIZE + (catalog.type === "csw" ? 1 : 0), PAGE_SIZE, text))
59+
.map((result) => ({result, records: getCatalogRecords(catalog.type, result || [])}))
60+
.map(resToProps);
61+
const scrollSpyOptions = {querySelector: ".ms2-border-layout-body", pageSize: PAGE_SIZE};
62+
/**
63+
* Compat catalog : Reusable catalog component, with infinite scroll.
64+
* You can simply pass the catalog to browse and the handler onRecordSelected.
65+
* @example
66+
* <CompactCatalog catalog={type: "csw", url: "..."} onSelected={selected => console.log(selected)} />
67+
* @name CompactCatalog
68+
* @memberof components.catalog
69+
* @prop {object} catalog the definition of the selected catalog as `{type: "wms"|"wmts"|"csw", url: "..."}`
70+
* @prop {object} selected the record selected. Passing this will show it as selected (highlighted) in the list. It will compare record's `identifier` property to guess the selected record in the list
71+
* @prop {function} onRecordSelected
72+
* @prop {boolean} showCatalogSelector if true shows the catalog selector - TODO
73+
* @prop {array} services TODO allow selection of catalog from a list
74+
* @prop {string} [searchText] the search text (if you want to control it)
75+
* @prop {function} [setSearchText] handler to get search text changes (if not defined, the component will control the text by it's own)
76+
*/
77+
module.exports = compose(
78+
withControllableState('searchText', "setSearchText", ""),
79+
withVirtualScroll({loadPage, scrollSpyOptions}),
80+
mapPropsStream( props$ =>
81+
props$.merge(props$.take(1).switchMap(({catalog, loadFirst = () => {} }) =>
82+
props$
83+
.debounceTime(500)
84+
.startWith({searchText: "", catalog})
85+
.distinctUntilKeyChanged('searchText')
86+
.do(({searchText, catalog: nextCatalog} = {}) => loadFirst({text: searchText, catalog: nextCatalog}))
87+
.ignoreElements() // don't want to emit props
88+
)))
89+
90+
)(({ setSearchText = () => { }, selected, onRecordSelected, loading, searchText, items = [], total, catalog, services, showCatalogSelector}) => {
91+
return (<BorderLayout
92+
className="compat-catalog"
93+
header={<CatalogForm services={services ? services : [catalog]} showCatalogSelector={showCatalogSelector} title={<Message msgId={"catalog.title"} />} searchText={searchText} onSearchTextChange={setSearchText}/>}
94+
footer={<div className="catalog-footer">
95+
<span>{loading ? <LoadingSpinner /> : null}</span>
96+
{!isNil(total) ? <span className="res-info"><Message msgId="catalog.pageInfoInfinite" msgParams={{loaded: items.length, total}}/></span> : null}
97+
</div>}>
98+
<SideGrid
99+
items={items.map(i =>
100+
i === selected
101+
|| selected
102+
&& i && i.record
103+
&& selected.identifier === i.record.identifier
104+
? {...i, selected: true}
105+
: i)}
106+
loading={loading}
107+
onItemClick={({record} = {}) => onRecordSelected(record, catalog)}/>
108+
</BorderLayout>);
109+
});

web/client/components/catalog/RecordItem.jsx

Lines changed: 16 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,14 @@ const PropTypes = require('prop-types');
1010
const SharingLinks = require('./SharingLinks');
1111
const Message = require('../I18N/Message');
1212
const {Image, Panel, Button, Glyphicon} = require('react-bootstrap');
13-
const {head, memoize, isObject} = require('lodash');
14-
const assign = require('object-assign');
13+
const {isObject} = require('lodash');
1514

1615
const CoordinatesUtils = require('../../utils/CoordinatesUtils');
1716
const ConfigUtils = require('../../utils/ConfigUtils');
17+
const {getRecordLinks, recordToLayer, extractOGCServicesReferences, buildSRSMap, removeParameters} = require('../../utils/CatalogUtils');
1818

1919
const defaultThumb = require('./img/default.jpg');
2020

21-
const buildSRSMap = memoize((srs) => {
22-
return srs.reduce((previous, current) => {
23-
return assign(previous, {[current]: true});
24-
}, {});
25-
});
26-
27-
const removeParameters = (url, skip) => {
28-
const urlparts = url.split('?');
29-
const params = {};
30-
if (urlparts.length >= 2 && urlparts[1]) {
31-
const pars = urlparts[1].split(/[&;]/g);
32-
pars.forEach((par) => {
33-
const param = par.split('=');
34-
if (skip.indexOf(param[0].toLowerCase()) === -1) {
35-
params[param[0]] = param[1];
36-
}
37-
});
38-
}
39-
return {url: urlparts[0], params};
40-
};
41-
4221
require("./RecordItem.css");
4322

4423
class RecordItem extends React.Component {
@@ -82,38 +61,6 @@ class RecordItem extends React.Component {
8261
document.removeEventListener('click', this.handleClick, false);
8362
}
8463

85-
getLinks = (record) => {
86-
let wmsGetCap = head(record.references.filter(reference => reference.type &&
87-
reference.type.indexOf("OGC:WMS") > -1 && reference.type.indexOf("http-get-capabilities") > -1));
88-
let wfsGetCap = head(record.references.filter(reference => reference.type &&
89-
reference.type.indexOf("OGC:WFS") > -1 && reference.type.indexOf("http-get-capabilities") > -1));
90-
let wmtsGetCap = head(record.references.filter(reference => reference.type &&
91-
reference.type.indexOf("OGC:WMTS") > -1 && reference.type.indexOf("http-get-capabilities") > -1));
92-
let links = [];
93-
if (wmsGetCap) {
94-
links.push({
95-
type: "WMS_GET_CAPABILITIES",
96-
url: wmsGetCap.url,
97-
labelId: 'catalog.wmsGetCapLink'
98-
});
99-
}
100-
if (wmtsGetCap) {
101-
links.push({
102-
type: "WMTS_GET_CAPABILITIES",
103-
url: wmtsGetCap.url,
104-
labelId: 'catalog.wmtsGetCapLink'
105-
});
106-
}
107-
if (wfsGetCap) {
108-
links.push({
109-
type: "WFS_GET_CAPABILITIES",
110-
url: wfsGetCap.url,
111-
labelId: 'catalog.wfsGetCapLink'
112-
});
113-
}
114-
return links;
115-
};
116-
11764
getTitle = (title) => {
11865
return isObject(title) ? title[this.props.currentLocale] || title.default : title || '';
11966
};
@@ -131,12 +78,10 @@ class RecordItem extends React.Component {
13178
return null;
13279
}
13380
// let's extract the references we need
134-
let wms = head(record.references.filter(reference => reference.type && (reference.type === "OGC:WMS"
135-
|| reference.type.indexOf("OGC:WMS") > -1 && reference.type.indexOf("http-get-map") > -1)));
136-
let wmts = head(record.references.filter(reference => reference.type && (reference.type === "OGC:WMTS"
137-
|| reference.type.indexOf("OGC:WMTS") > -1 && reference.type.indexOf("http-get-map") > -1)));
81+
const {wms, wmts} = extractOGCServicesReferences(record);
13882
// let's create the buttons
13983
let buttons = [];
84+
// TODO addLayer and addwmtsLayer do almost the same thing and they should be unified
14085
if (wms) {
14186
buttons.push(
14287
<Button
@@ -163,9 +108,9 @@ class RecordItem extends React.Component {
163108
</Button>
164109
);
165110
}
166-
// creating get capbilities links that will be used to share layers info
111+
// create get capabilities links that will be used to share layers info
167112
if (this.props.showGetCapLinks) {
168-
let links = this.getLinks(record);
113+
let links = getRecordLinks(record);
169114
if (links.length > 0) {
170115
buttons.push(<SharingLinks key="sharing-links" popoverContainer={this} links={links}
171116
onCopy={this.props.onCopy} buttonSize={this.props.buttonSize} addAuthentication={this.props.addAuthentication}/>);
@@ -214,33 +159,16 @@ class RecordItem extends React.Component {
214159
};
215160

216161
addLayer = (wms) => {
217-
const {url, params} = removeParameters(ConfigUtils.cleanDuplicatedQuestionMarks(wms.url), ["request", "layer", "service", "version"].concat(this.props.authkeyParamNames));
162+
const {url} = removeParameters(ConfigUtils.cleanDuplicatedQuestionMarks(wms.url), ["request", "layer", "service", "version"].concat(this.props.authkeyParamNames));
218163
const allowedSRS = buildSRSMap(wms.SRS);
219164
if (wms.SRS.length > 0 && !CoordinatesUtils.isAllowedSRS(this.props.crs, allowedSRS)) {
220165
this.props.onError('catalog.srs_not_allowed');
221166
} else {
222-
this.props.onLayerAdd({
223-
type: "wms",
224-
url: url,
225-
visibility: true,
226-
dimensions: this.props.record.dimensions || [],
227-
name: wms.params && wms.params.name,
228-
title: this.props.record.title || wms.params && wms.params.name,
229-
description: this.props.record.description || "",
230-
bbox: {
231-
crs: this.props.record.boundingBox.crs,
232-
bounds: {
233-
minx: this.props.record.boundingBox.extent[0],
234-
miny: this.props.record.boundingBox.extent[1],
235-
maxx: this.props.record.boundingBox.extent[2],
236-
maxy: this.props.record.boundingBox.extent[3]
237-
}
238-
},
239-
links: this.getLinks(this.props.record),
240-
params: params,
241-
allowedSRS: allowedSRS,
242-
catalogURL: this.props.catalogType === 'csw' ? this.props.catalogURL + "?request=GetRecordById&service=CSW&version=2.0.2&elementSetName=full&id=" + this.props.record.identifier : null
243-
});
167+
this.props.onLayerAdd(
168+
recordToLayer(this.props.record, "wms", {
169+
url,
170+
catalogURL: this.props.catalogType === 'csw' && this.props.catalogURL ? this.props.catalogURL + "?request=GetRecordById&service=CSW&version=2.0.2&elementSetName=full&id=" + this.props.record.identifier : null
171+
}));
244172
if (this.props.record.boundingBox && this.props.zoomToLayer) {
245173
let extent = this.props.record.boundingBox.extent;
246174
let crs = this.props.record.boundingBox.crs;
@@ -250,33 +178,14 @@ class RecordItem extends React.Component {
250178
};
251179

252180
addwmtsLayer = (wmts) => {
253-
const {url, params} = removeParameters(ConfigUtils.cleanDuplicatedQuestionMarks(wmts.url), ["request", "layer"].concat(this.props.authkeyParamNames));
181+
const {url} = removeParameters(ConfigUtils.cleanDuplicatedQuestionMarks(wmts.url), ["request", "layer"].concat(this.props.authkeyParamNames));
254182
const allowedSRS = buildSRSMap(wmts.SRS);
255183
if (wmts.SRS.length > 0 && !CoordinatesUtils.isAllowedSRS(this.props.crs, allowedSRS)) {
256184
this.props.onError('catalog.srs_not_allowed');
257185
} else {
258-
this.props.onLayerAdd({
259-
type: "wmts",
260-
url: url,
261-
visibility: true,
262-
name: wmts.params && wmts.params.name,
263-
title: this.props.record.title || wmts.params && wmts.params.name,
264-
matrixIds: this.props.record.matrixIds || [],
265-
description: this.props.record.description || "",
266-
tileMatrixSet: this.props.record.tileMatrixSet || [],
267-
bbox: {
268-
crs: this.props.record.boundingBox.crs,
269-
bounds: {
270-
minx: this.props.record.boundingBox.extent[0],
271-
miny: this.props.record.boundingBox.extent[1],
272-
maxx: this.props.record.boundingBox.extent[2],
273-
maxy: this.props.record.boundingBox.extent[3]
274-
}
275-
},
276-
links: this.getLinks(this.props.record),
277-
params: params,
278-
allowedSRS: allowedSRS
279-
});
186+
this.props.onLayerAdd(recordToLayer(this.props.record, "wmts", {
187+
url
188+
}));
280189
if (this.props.record.boundingBox && this.props.zoomToLayer) {
281190
let extent = this.props.record.boundingBox.extent;
282191
let crs = this.props.record.boundingBox.crs;

0 commit comments

Comments
 (0)