Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/geospatial interface #841

Draft
wants to merge 32 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7ccf4fb
initial setup of geospatial interface config
buckhalt Dec 19, 2024
6ef86c3
prompt fields section
buckhalt Dec 19, 2024
2179524
basic section layout
buckhalt Dec 20, 2024
61c0fa1
wip add geojson files
buckhalt Jan 7, 2025
c836287
implement propToSelect using asset selectors to parse geojson
buckhalt Jan 7, 2025
94fcfaa
implement color palette
buckhalt Jan 8, 2025
4da3662
working prompt config
buckhalt Jan 8, 2025
591d61e
upload txt file for mapbox key
buckhalt Jan 8, 2025
8a29011
prop validation, add GeoAPIKey form field
buckhalt Jan 9, 2025
bd4e44c
improve instructions and validations
buckhalt Jan 9, 2025
e29e07e
wip proof of concept mapbox key flow
buckhalt Jan 9, 2025
a73ff18
wip edit functionality
buckhalt Jan 10, 2025
ce1a8ae
refactor create key form to use basicForm & handle edits
buckhalt Jan 13, 2025
2fb138e
conditionally render saveButton only when form is dirty
buckhalt Jan 13, 2025
27c1375
update prop names, native select for target feature prop
buckhalt Jan 14, 2025
0011a11
refactor: save api key directly to asset manifest
buckhalt Jan 14, 2025
36cad69
add geo/api thumbnails, api preview
buckhalt Jan 15, 2025
5a688f7
fix preview in field
buckhalt Jan 15, 2025
30ee676
fix asset selected states
buckhalt Jan 15, 2025
e45b488
wip mapbox map for center, zoom selection
buckhalt Jan 15, 2025
558ff60
fix connect-src, rm unused stuff
buckhalt Jan 15, 2025
03e524e
fix csp to use simpler, not strict setup
buckhalt Jan 16, 2025
3a57fae
update mapbox to v3
buckhalt Jan 16, 2025
652ca58
working map as field to save center and zoom
buckhalt Jan 16, 2025
eb73e50
implement selecting style, conditionally render save button
buckhalt Jan 16, 2025
5f28c89
update mapbox to v3, use es6 import and target unminified code
buckhalt Jan 16, 2025
2cf85f9
small fixes
buckhalt Jan 16, 2025
d598829
geojson preview using reusable Table component, make network use tabl…
buckhalt Jan 16, 2025
d061430
disable download, implement copy for api keys
buckhalt Jan 16, 2025
995825a
add geospatial timeline image
buckhalt Jan 16, 2025
7353816
add zoom buttons for better ux
buckhalt Jan 16, 2025
17e2ace
change case of apiKey to apikey for consistency w Fresco
buckhalt Jan 22, 2025
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
242 changes: 237 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,5 +269,8 @@
"@utils/(.*)$": "<rootDir>/src/utils/$1",
"^react-native$": "react-native-web"
}
},
"dependencies": {
"mapbox-gl": "^3.9.3"
}
}
12 changes: 7 additions & 5 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
http-equiv="Content-Security-Policy"
content="
default-src 'self';
connect-src 'self' https://api.github.com:* http://localhost:* https://localhost:* https://documentation.networkcanvas.com:* https://assets.networkcanvas.com:* %REACT_APP_CONNECT_SRC_CSP%;
script-src 'self' %REACT_APP_SCRIPT_SRC_CSP%;
style-src 'self' 'unsafe-inline';
media-src 'self' data: protocol: asset:;
img-src 'self' data: protocol: asset:;
worker-src blob: ;
child-src blob: ;
connect-src 'self' https://api.github.com:* http://localhost:* https://localhost:* https://documentation.networkcanvas.com:* https://assets.networkcanvas.com:* https://*.tiles.mapbox.com https://api.mapbox.com https://events.mapbox.com %REACT_APP_CONNECT_SRC_CSP% ;
script-src 'self' %REACT_APP_SCRIPT_SRC_CSP% ;
style-src 'self' 'unsafe-inline' ;
media-src 'self' data: protocol: asset: ;
img-src 'self' data: blob: protocol: asset: ;
"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
Expand Down
3 changes: 3 additions & 0 deletions src/components/AssetBrowser/Asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const ASSET_COMPONENTS = {
video: Thumbnails.Video,
audio: Thumbnails.Audio,
network: Thumbnails.Network,
geospatial: Thumbnails.GeoJSON,
apikey: Thumbnails.APIKey,
geojson: Thumbnails.GeoJSON,
};

const Asset = ({
Expand Down
37 changes: 22 additions & 15 deletions src/components/AssetBrowser/Assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const ASSET_TYPES = [
{ label: 'Video', value: 'video' },
{ label: 'Audio', value: 'audio' },
{ label: 'Network', value: 'network' },
{ label: 'GeoJSON', value: 'geospatial' },
{ label: 'API Key', value: 'apikey' },
];

const Assets = ({
Expand All @@ -32,21 +34,26 @@ const Assets = ({
source,
type: thumbnailType,
isUsed,
}) => (
<div className="asset-browser-assets__asset" key={id}>
<Asset
id={id}
name={name}
source={source}
type={thumbnailType}
isUsed={isUsed}
onClick={onSelect}
onPreview={onPreview}
onDownload={onDownload}
onDelete={handleDelete}
/>
</div>
));
}) => {
// disable download for apikey type
const handleDownload = (thumbnailType === 'apikey') ? null : onDownload;

return (
<div className="asset-browser-assets__asset" key={id}>
<Asset
id={id}
name={name}
source={source}
type={thumbnailType}
isUsed={isUsed}
onClick={onSelect}
onPreview={onPreview}
onDownload={handleDownload}
onDelete={handleDelete}
/>
</div>
);
});

return (
<div className="asset-browser-assets">
Expand Down
22 changes: 21 additions & 1 deletion src/components/AssetBrowser/Preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { compose } from 'redux';
import cx from 'classnames';
import { Button } from '@codaco/ui';
import Window from '@components/Window';
import ContentCopyIcon from '@material-ui/icons/FileCopy';
import DownloadIcon from '@material-ui/icons/GetApp';
import withAssetMeta from '@components/Assets/withAssetMeta';
import withAssetPath from '@components/Assets/withAssetPath';
Expand All @@ -21,6 +22,10 @@ const getRenderer = (meta) => {
return ({ id }) => <Assets.Video id={id} controls />;
case 'network':
return Assets.Network;
case 'geospatial':
return Assets.GeoJSON;
case 'apikey':
return Assets.APIKey;
default:
return () => <p>No preview available.</p>;
}
Expand All @@ -39,6 +44,12 @@ const Preview = ({
onDownload(assetPath, meta);
}, [onDownload, assetPath, meta]);

const handleCopyKey = useCallback(() => {
if (meta.value) {
navigator.clipboard.writeText(meta.value);
}
}, []);

const primaryButtons = [
<Button
onClick={onClose}
Expand All @@ -49,14 +60,23 @@ const Preview = ({
</Button>,
];

const secondaryButtons = [
// API keys are copied instead of downloaded
const secondaryButtons = meta.type !== 'apikey' ? [
<Button
onClick={handleDownload}
icon={<DownloadIcon />}
key="download"
>
Download asset
</Button>,
] : [
<Button
onClick={handleCopyKey}
icon={<ContentCopyIcon />}
key="copy"
>
Copy API Key
</Button>,
];

const className = cx(
Expand Down
1 change: 1 addition & 0 deletions src/components/AssetBrowser/withAssetActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const connectActions = connect(
null,
{
deleteAsset: assetActions.deleteAsset,
importAsset: assetActions.importAsset,
openDialog: dialogActions.openDialog,
},
);
Expand Down
24 changes: 24 additions & 0 deletions src/components/Assets/APIKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import withAssetMeta from './withAssetMeta';

const APIKey = ({ meta }) => (
<h1 style={{ wordWrap: 'break-word' }}>
{meta.value}
</h1>
);

APIKey.propTypes = {
meta: PropTypes.shape({
value: PropTypes.string,
name: PropTypes.string,
}),
};

APIKey.defaultProps = {
meta: {
value: '',
},
};

export default withAssetMeta(APIKey);
58 changes: 58 additions & 0 deletions src/components/Assets/GeoJSON.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useState, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';

import { get } from 'lodash';
import fs from 'fs-extra';
import Table from './Table';
import withAssetPath from './withAssetPath';

const initialContent = {
geojson: { features: [] },
columns: [],
};

const getGeoJSON = (assetPath) => fs.readJson(assetPath);

const getRows = (geojson) => get(geojson, ['features'], []).map(
({ properties }) => properties,
);

const getColumns = (geojson) => {
const properties = get(geojson, ['features'], []).map(
(feature) => feature.properties,
);

const columnNames = Array.from(
new Set(properties.flatMap(Object.keys)),
);

return columnNames.map((col) => ({
Header: col,
accessor: col,
}));
};

const GeoJSONTable = ({ assetPath }) => {
const [content, setContent] = useState({ ...initialContent });

useEffect(() => {
if (!assetPath) {
setContent({ ...initialContent });
return;
}

getGeoJSON(assetPath)
.then(setContent);
}, [assetPath]);

const data = useMemo(() => getRows(content), [content]);
const columns = useMemo(() => getColumns(content), [content]);

return <Table data={data} columns={columns} />;
};

GeoJSONTable.propTypes = {
assetPath: PropTypes.string.isRequired,
};

export default withAssetPath(GeoJSONTable);
60 changes: 2 additions & 58 deletions src/components/Assets/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import React, { useState, useEffect, useMemo } from 'react';
import { compose } from 'redux';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { useTable, useSortBy } from 'react-table';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';
import { getVariableNamesFromNetwork } from '@app/protocol-validation/validation/validateExternalData';
import withAssetPath from './withAssetPath';
import { networkReader } from '../../utils/protocols/assetTools';
import Table from './Table';

const initialContent = {
network: { nodes: [] },
Expand All @@ -26,15 +24,6 @@ const getColumns = (network) => getVariableNamesFromNetwork(network).map(
}),
);

const getSortIcon = (column) => {
if (!column.isSorted) { return null; }
return (
column.isSortedDesc
? <ArrowDropDownIcon />
: <ArrowDropUpIcon />
);
};

const Network = ({ assetPath }) => {
const [content, setContent] = useState({ ...initialContent });

Expand All @@ -51,60 +40,15 @@ const Network = ({ assetPath }) => {
const data = useMemo(() => getRows(content), [content]);
const columns = useMemo(() => getColumns(content), [content]);

const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable(
{ data, columns },
useSortBy,
);

return (
<table {...getTableProps()} className="network">
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th
{...column.getHeaderProps(column.getSortByToggleProps())}
>
{column.render('Header')}
{getSortIcon(column)}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row) => {
prepareRow(row);

return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => (
<td
{...cell.getCellProps()}
>
{cell.render('Cell')}
</td>
))}
</tr>
);
})}
</tbody>
</table>
<Table data={data} columns={columns} />
);
};

Network.propTypes = {
assetPath: PropTypes.string.isRequired,
};

export { Network };

export default compose(
withAssetPath,
)(Network);
Loading
Loading