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
15 changes: 15 additions & 0 deletions packages/client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -14102,6 +14102,21 @@ select{
text-decoration-color:#94a3b8
}

.decoration-white{
-webkit-text-decoration-color:#fff;
text-decoration-color:#fff
}

.decoration-amber-700{
-webkit-text-decoration-color:#b45309;
text-decoration-color:#b45309
}

.decoration-blue-800{
-webkit-text-decoration-color:#1e40af;
text-decoration-color:#1e40af
}

.decoration-dotted{
-webkit-text-decoration-style:dotted;
text-decoration-style:dotted
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/reports/ReportCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,9 @@ export default function ReportCard(
geographies: baseReportContext.geographies,
sketchClass: subjectSketchClass ?? null,
errors: cardDependencies.errors,
globalErrors: cardDependencies.globalErrors,
dependenciesAwaitingRefresh:
cardDependencies.dependenciesAwaitingRefresh,
dependencyResolutionFailuresByHash:
cardDependencies.dependencyResolutionFailuresByHash,
}}
Expand Down
25 changes: 25 additions & 0 deletions packages/client/src/reports/ReportEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,16 @@ export default function ReportEditor({

const [editing, _setEditing] = useState<number | null>(null);
const [preselectTitle, setPreselectTitle] = useState<boolean>(false);
const [pendingWidgetSettings, setPendingWidgetSettings] = useState<{
cardId: number;
widgetPosition: number;
} | null>(null);
const setEditing = useCallback(
(editing: number | null, preselectTitle?: boolean) => {
setPreselectTitle(preselectTitle || false);
if (editing === null) {
setPendingWidgetSettings(null);
}
_setEditing(editing);
},
[_setEditing, setPreselectTitle]
Expand Down Expand Up @@ -451,6 +458,18 @@ export default function ReportEditor({
[calcDetailsModalState]
);

const requestWidgetSettings = useCallback(
(cardId: number, widgetPosition: number) => {
setPendingWidgetSettings({ cardId, widgetPosition });
setEditing(cardId);
},
[setEditing]
);

const clearPendingWidgetSettings = useCallback(() => {
setPendingWidgetSettings(null);
}, []);

const onEditorReadyForFocus = useCallback(
(cardId: number, focus: () => void) => {
// Ref avoids a race where context still holds a callback from before
Expand Down Expand Up @@ -485,6 +504,9 @@ export default function ReportEditor({
preselectTitle: preselectTitle,
showCalcDetails: calcDetailsModalState.state.cardId ?? undefined,
setShowCalcDetails: setShowCalcDetails,
requestWidgetSettings,
pendingWidgetSettings,
clearPendingWidgetSettings,
onEditorReadyForFocus,
printing,
setPrinting,
Expand All @@ -498,6 +520,9 @@ export default function ReportEditor({
preselectTitle,
calcDetailsModalState.state.cardId,
setShowCalcDetails,
requestWidgetSettings,
pendingWidgetSettings,
clearPendingWidgetSettings,
onEditorReadyForFocus,
printing,
requestFullReportPrint,
Expand Down
128 changes: 128 additions & 0 deletions packages/client/src/reports/components/MetricSuggestedFixes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { ExternalLinkIcon } from "@radix-ui/react-icons";
import { MetricDependency } from "overlay-engine";
import { FC } from "react";
import { useTranslation } from "react-i18next";

type MetricSuggestionContext = {
type?: string;
parameters?: Record<string, any> | null;
};

function addSuggestedFixesForError(
suggestedFixes: Record<string, string>,
error: string,
contexts: MetricSuggestionContext[]
) {
if (
error.includes("Invalid array length") &&
contexts.some((context) => context.type === "raster_stats")
) {
suggestedFixes["Adjust VRM settings"] =
"https://docs.seasketch.org/seasketch-documentation/administrators-guide/reports/debugging-timeout-and-performance-issues#raster-vrm-settings";
} else if (/timeout/i.test(error)) {
const overlapContext = contexts.find(
(context) =>
context.type === "overlay_area" ||
context.parameters?.sourceHasOverlappingFeatures === true
);
if (overlapContext) {
suggestedFixes["Adjust overlap settings"] =
"https://docs.seasketch.org/seasketch-documentation/administrators-guide/reports/debugging-timeout-and-performance-issues#overlapping-polygon-settings";
} else {
suggestedFixes["Review report performance documentation"] =
"https://docs.seasketch.org/seasketch-documentation/administrators-guide/reports/debugging-timeout-and-performance-issues";
}
}
}

export function getMetricErrorInfo(
errors: string[],
dependencies: MetricDependency[]
) {
const errorMap: Record<string, number> = {};
for (const error of errors) {
if (error in errorMap) {
errorMap[error]++;
} else {
errorMap[error] = 1;
}
}

const suggestedFixes: Record<string, string> = {};
const contexts = dependencies.map((dependency) => ({
type: dependency.type,
parameters: dependency.parameters,
}));
for (const error of Object.keys(errorMap)) {
addSuggestedFixesForError(suggestedFixes, error, contexts);
}

return { errorMap, suggestedFixes };
}

export function getSuggestedFixesForMetricError({
error,
metricType,
parameters,
}: {
error?: string | null;
metricType?: string;
parameters?: Record<string, any> | null;
}) {
const suggestedFixes: Record<string, string> = {};
if (!error) {
return suggestedFixes;
}
addSuggestedFixesForError(suggestedFixes, error, [
{ type: metricType, parameters },
]);
return suggestedFixes;
}

export const MetricSuggestedFixes: FC<{
suggestedFixes: Record<string, string>;
compact?: boolean;
theme?: "light" | "dark";
}> = ({ suggestedFixes, compact, theme = "light" }) => {
const { t } = useTranslation("reports");
const entries = Object.entries(suggestedFixes);
if (entries.length === 0) {
return null;
}
const dark = theme === "dark";

return (
<div
className={`rounded-md border ${
dark ? "border-gray-700 bg-gray-800/80" : "border-gray-200 bg-gray-50"
} ${compact ? "mt-2 px-2.5 py-2" : "mt-3 px-3 py-2.5"}`}
>
<div
className={`font-semibold ${dark ? "text-gray-100" : "text-gray-700"} ${
compact ? "" : "text-sm"
}`}
>
{t("Suggested fixes")}
</div>
<div className={`${compact ? "mt-1" : "mt-1.5 text-sm"} space-y-1`}>
{entries.map(([msg, url]) => (
<div key={msg}>
<a
className={`inline-flex items-center gap-1 rounded !underline underline-offset-2 ${
dark
? "text-blue-300 hover:text-blue-200"
: "text-blue-600 hover:text-blue-800"
}`}
href={url}
target="_blank"
rel="noopener noreferrer"
>
{msg}
<ExternalLinkIcon className="h-3 w-3 shrink-0" />
</a>
</div>
))}
</div>
</div>
);
};
39 changes: 36 additions & 3 deletions packages/client/src/reports/components/ReportCardBodyEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EditorState, TextSelection } from "prosemirror-state";
import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
import { Node } from "prosemirror-model";
import { EditorView } from "prosemirror-view";
import {
Expand Down Expand Up @@ -151,6 +151,8 @@ function ReportCardBodyEditorInner({
setShowCalcDetails,
showCalcDetails,
onEditorReadyForFocus,
pendingWidgetSettings,
clearPendingWidgetSettings,
} = useContext(ReportUIStateContext);

const onEditorReadyForFocusRef = useRef(onEditorReadyForFocus);
Expand Down Expand Up @@ -286,7 +288,7 @@ function ReportCardBodyEditorInner({
const anySpatialMetricPending = slim.some(
(m) =>
m.state !== SpatialMetricState.Complete &&
m.state !== SpatialMetricState.Error,
m.state !== SpatialMetricState.Error
);
if (
!draftDependenciesQuery.loading &&
Expand Down Expand Up @@ -367,7 +369,7 @@ function ReportCardBodyEditorInner({
return {} as { [dependencyHash: string]: string };
}
return Object.fromEntries(
errs.map((e) => [e.dependencyHash, e.message] as const),
errs.map((e) => [e.dependencyHash, e.message] as const)
);
}, [
draftDependenciesQuery.data?.draftReportDependencies
Expand Down Expand Up @@ -695,6 +697,37 @@ function ReportCardBodyEditorInner({
saveWithBody,
]);

useEffect(() => {
if (!pendingWidgetSettings || pendingWidgetSettings.cardId !== cardId) {
return;
}
const view = viewRef.current;
if (!view) {
return;
}
const { widgetPosition } = pendingWidgetSettings;
if (widgetPosition < 0 || widgetPosition > view.state.doc.content.size) {
clearPendingWidgetSettings();
return;
}
const node = view.state.doc.nodeAt(widgetPosition);
if (
!node ||
(node.type.name !== "metric" && node.type.name !== "blockMetric")
) {
clearPendingWidgetSettings();
return;
}
const tr = view.state.tr.setSelection(
NodeSelection.create(view.state.doc, widgetPosition)
);
view.dispatch(tr);
clearPendingWidgetSettings();
setTimeout(() => {
view.dom.focus({ preventScroll: true });
}, 30);
}, [cardId, clearPendingWidgetSettings, pendingWidgetSettings, viewRef]);

// Update editor state when language changes (body will be different for different languages)
useEffect(() => {
if (viewRef.current && body) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,23 @@ export function computeCombinedProgress(items: MinimalJob[]): {
progressPercent: number | null; // null => indeterminate
farthestEta: Date | null;
thresholdMet: boolean;
allComplete: boolean;
allSettled: boolean;
} {
const N = items.length;
if (N === 0) {
return {
progressPercent: null,
farthestEta: null,
thresholdMet: false,
allComplete: true,
allSettled: true,
};
}

const isSettled = (state: SpatialMetricState) =>
state === SpatialMetricState.Complete || state === SpatialMetricState.Error;

const withEtaOrComplete = items.filter(
(i) => i.eta !== null || i.state === SpatialMetricState.Complete
(i) => i.eta !== null || isSettled(i.state)
).length;
const thresholdMet = withEtaOrComplete / N >= 0.75;

Expand All @@ -74,16 +77,14 @@ export function computeCombinedProgress(items: MinimalJob[]): {
null;

const started = items.filter((i) => i.progress > 0);
const allComplete = items.every(
(i) => i.state === SpatialMetricState.Complete
);
const allSettled = items.every((i) => isSettled(i.state));

if (allComplete) {
if (allSettled) {
return {
progressPercent: 100,
farthestEta: null,
thresholdMet,
allComplete,
allSettled,
};
}

Expand All @@ -93,7 +94,7 @@ export function computeCombinedProgress(items: MinimalJob[]): {
progressPercent: null,
farthestEta: null,
thresholdMet,
allComplete,
allSettled,
};
}
const minProgress = started.reduce(
Expand All @@ -109,7 +110,7 @@ export function computeCombinedProgress(items: MinimalJob[]): {
progressPercent: scaledProgress,
farthestEta: null,
thresholdMet,
allComplete,
allSettled,
};
}

Expand All @@ -122,7 +123,7 @@ export function computeCombinedProgress(items: MinimalJob[]): {
progressPercent: farthestItem.progress,
farthestEta,
thresholdMet,
allComplete,
allSettled,
};
}

Expand All @@ -132,7 +133,7 @@ export function computeCombinedProgress(items: MinimalJob[]): {
progressPercent: null,
farthestEta: null,
thresholdMet,
allComplete,
allSettled,
};
}
const minProgress = started.reduce(
Expand All @@ -143,7 +144,7 @@ export function computeCombinedProgress(items: MinimalJob[]): {
progressPercent: minProgress,
farthestEta: null,
thresholdMet,
allComplete,
allSettled,
};
}

Expand Down Expand Up @@ -188,7 +189,7 @@ export default function ReportCardLoadingIndicator({
}, [stage, sourceJobs, metricJobs]);

const isComplete =
!anySourceInProgress && computeCombinedProgress(metricJobs).allComplete;
!anySourceInProgress && computeCombinedProgress(metricJobs).allSettled;

// Enforce monotonic non-decreasing visual progress with phase-aware reset
const prevPercentRef = useRef<number>(0);
Expand Down
Loading
Loading