Skip to content

Commit

Permalink
WIP form admin
Browse files Browse the repository at this point in the history
  • Loading branch information
underbluewaters committed Oct 30, 2024
1 parent d12ea51 commit 0a74cd5
Show file tree
Hide file tree
Showing 13 changed files with 774 additions and 84 deletions.
5 changes: 5 additions & 0 deletions packages/api/generated-schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -13162,6 +13162,8 @@ type SketchClass implements Node {
sketch classes can only be digitized by admins.
"""
canDigitize: Boolean
filterApiServerLocation: String
filterApiVersion: Int!

"""Reads a single `Form` that is related to this `SketchClass`."""
form: Form
Expand Down Expand Up @@ -13304,6 +13306,8 @@ input SketchClassPatch {
"""
allowMulti: Boolean
filterApiServerLocation: String
filterApiVersion: Int

"""
Geometry type users digitize. COLLECTION types act as a feature collection and have no drawn geometry.
Expand Down Expand Up @@ -13439,6 +13443,7 @@ enum SketchFoldersOrderBy {
enum SketchGeometryType {
CHOOSE_FEATURE
COLLECTION
FILTERED_PLANNING_UNITS
LINESTRING
POINT
POLYGON
Expand Down
44 changes: 43 additions & 1 deletion packages/api/migrations/current.sql
Original file line number Diff line number Diff line change
@@ -1 +1,43 @@
-- Enter migration here
-- Enter migration here
alter table sketch_classes add column if not exists filter_api_version int not null default 1;
alter table sketch_classes add column if not exists filter_api_server_location text;

alter type sketch_geometry_type add value if not exists 'FILTERED_PLANNING_UNITS';

set role seasketch_superuser;
delete from sketch_classes where project_id = (select id from projects where slug = 'superuser') and name = 'Filtered Planning Units';

insert into sketch_classes(
project_id,
name,
geometry_type,
mapbox_gl_style,
is_template,
template_description
) values (
(select id from projects where slug = 'superuser'),
'Filtered Planning Units',
'FILTERED_PLANNING_UNITS',
'{}'::jsonb,
true,
'Filter polygons by criteria. Requires an API server.'
) on conflict do nothing;

select initialize_sketch_class_form_from_template((select id from sketch_classes where name = 'Filtered Planning Units' and is_template = true), (select id from forms where is_template = true and template_type = 'SKETCHES' and template_name = 'Basic Template'));
set role postgres;

GRANT update (filter_api_server_location) on sketch_classes to seasketch_user;
GRANT update (filter_api_version) on sketch_classes to seasketch_user;

delete from form_element_types where component_name = 'FilterInput';
insert into form_element_types (
component_name,
label,
is_input,
is_surveys_only
) values (
'FilterInput',
'Filter Input',
true,
false
);
161 changes: 161 additions & 0 deletions packages/client/src/admin/sketchClasses/EvaluateFilterServiceModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Trans, useTranslation } from "react-i18next";
import Modal from "../../components/Modal";
import { GeostatsAttribute } from "@seasketch/geostats-types";
import { useEffect, useState } from "react";
import Warning from "../../components/Warning";

export default function EvaluateFilterServiceModal(props: {
location: string;
onRequestClose: () => void;
}) {
const { t } = useTranslation("admin:sketching");
const [state, setState] = useState({
loading: true,
attributes: [] as GeostatsAttribute[],
version: 0,
error: null as Error | null,
featureCount: 0,
});

useEffect(() => {
if (props.location) {
setState({
loading: true,
attributes: [],
version: 0,
error: null,
featureCount: 0,
});
const abortController = new AbortController();
fetch(`${props.location.replace(/\/$/, "")}/metadata`, {
signal: abortController.signal,
})
.then(async (res) => {
if (res.ok) {
const json = await res.json();
if (!json.version) {
throw new Error(
"Invalid response from filter service. Missing version."
);
}
if (!json.attributes) {
throw new Error(
"Invalid response from filter service. Missing attributes."
);
}
if (!Array.isArray(json.attributes)) {
throw new Error(
"Invalid response from filter service. Attributes must be an array."
);
}
const firstAttr = json.attributes[0];
if (
!firstAttr.attribute ||
typeof firstAttr.attribute !== "string"
) {
throw new Error(
"Invalid response from filter service. Attribute.attribute must be a string."
);
}
setState({
loading: true,
attributes: json.attributes,
version: json.version,
error: null,
featureCount: 0,
});
// next, count features
fetch(`${props.location.replace(/\/$/, "")}/count`, {
signal: abortController.signal,
})
.then(async (res) => {
if (res.ok) {
const json = await res.json();
setState((prev) => {
return {
...prev,
featureCount: json.count,
loading: false,
};
});
} else {
setState({
loading: false,
attributes: [],
version: 0,
error: new Error(await res.text()),
featureCount: 0,
});
}
})
.catch((err) => {
if (err.name !== "AbortError") {
setState({
loading: false,
attributes: [],
version: 0,
error: err,
featureCount: 0,
});
}
});
} else {
setState({
loading: false,
attributes: [],
version: 0,
error: new Error(await res.text()),
featureCount: 0,
});
}
})
.catch((err) => {
if (err.name !== "AbortError") {
setState({
loading: false,
attributes: [],
version: 0,
error: err,
featureCount: 0,
});
}
});
return () => {
abortController.abort();
};
}
}, [props.location]);

return (
<Modal
loading={state.loading}
onRequestClose={props.onRequestClose}
title={t("Evaluate Filter Service")}
>
{state.error ? (
<Warning level="error">{state.error.message}</Warning>
) : (
<div>
<p>
<Trans ns="admin:sketching">
This filter service is on version {{ version: state.version }} and
currently contains{" "}
{{ featureCount: state.featureCount.toLocaleString() }} features,
with the following attributes available for filtering:
</Trans>
</p>

<div className="max-h-64 overflow-y-auto p-2 border">
<ul>
{state.attributes.map((attr) => (
<li key={attr.attribute}>
<strong>{attr.attribute}</strong> ({attr.type})
</li>
))}
</ul>
</div>
</div>
)}
</Modal>
);
}
Loading

0 comments on commit 0a74cd5

Please sign in to comment.