Skip to content

Commit dbcd3a0

Browse files
authored
Merge pull request #714 from SlideRuleEarth/carlos-dev4
When updating the plot some errors do not alert the user, they only s…
2 parents 801fd0f + 4873a6c commit dbcd3a0

File tree

4 files changed

+197
-15
lines changed

4 files changed

+197
-15
lines changed

web-client/src/components/SrRasterParams.vue

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
@click="addRasterParams()">Add New Raster Params
1616
</Button>
1717
</div>
18-
<SrTextInput
18+
<SrSqlFieldInput
1919
label="Key"
2020
v-model="rasterParamsStore.key"
21+
tooltipText="user supplied name used to identify results returned from sampling this raster, must be SQL compliant and unique for each raster param set"
2122
/>
2223
<Select
2324
v-model="rasterParamsStore.asset"
@@ -39,15 +40,18 @@
3940
/>
4041
<SrSliderInput
4142
label="Radius"
42-
v-model="rasterParamsStore.radius"
43+
v-model="rasterParamsStore.radius"
44+
tooltipText="the size of the kernel in meters when sampling a raster; the size of the region in meters for zonal statistics"
4345
/>
4446
<SrCheckbox
4547
label="Zonal Stats"
46-
v-model="rasterParamsStore.zonalStats"
48+
v-model="rasterParamsStore.zonalStats"
49+
tooltipText="boolean whether to calculate and return zonal statistics for the region around the location being sampled"
4750
/>
4851
<SrCheckbox
4952
label="With Flags"
50-
v-model="rasterParamsStore.withFlags"
53+
v-model="rasterParamsStore.withFlags"
54+
tooltipText="boolean whether to include auxiliary information about the sampled pixel in the form of a 32-bit flag"
5155
/>
5256
<Sr32BitFlag
5357
:disabled="!rasterParamsStore.withFlags"
@@ -74,13 +78,15 @@
7478
:setValue="rasterParamsStore.setT1"
7579
/>
7680
</div>
77-
<SrTextInput
81+
<SrTextInput
7882
label="Substring"
79-
v-model="rasterParamsStore.substring"
83+
v-model="rasterParamsStore.substring"
84+
tooltipText="substring filter for rasters to be sampled; the raster will only be sampled if the name of the raster includes the provided substring (useful for datasets that have multiple rasters for a given geolocation to be sampled)"
8085
/>
8186
<SrCheckbox
8287
label="Use Closest Time"
8388
v-model="rasterParamsStore.useClosetTime"
89+
tooltipText="time used to filter rasters to be sampled; only the raster that is closest in time to the provided time will be sampled - can be multiple rasters if they all share the same time (format %Y-%m-%dT%H:%M:%SZ, e.g. 2018-10-13T00:00:00Z)"
8490
/>
8591
<SrCalendar
8692
label="Closest"
@@ -93,6 +99,7 @@
9399
<SrCheckbox
94100
label="Use POI Time"
95101
v-model="rasterParamsStore.use_poi_time"
102+
tooltipText="overrides the “closest_time” setting (or provides one if not set) with the time associated with the point of interest being sampled"
96103
/>
97104
<div class="sr-raster-params-catalog">
98105
<SrLabelInfoIconButton
@@ -142,6 +149,7 @@
142149
import SrLabelInfoIconButton from './SrLabelInfoIconButton.vue';
143150
import Sr32BitFlag from './Sr32BitFlag.vue';
144151
import { onMounted } from 'vue';
152+
import SrSqlFieldInput from './SrSqlFieldInput.vue';
145153
146154
const rasterParamsStore = useRasterParamsStore();
147155
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<template>
2+
<div class="sr-sql-field-input-wrapper">
3+
<div class="sr-text-row">
4+
<SrLabelInfoIconButton :label="label" :tooltipText="tooltipText" :tooltipUrl="tooltipUrl" :insensitive="insensitive" :labelFontSize="labelFontSize"/>
5+
<InputText v-model="modelValueComputed" class="input-text" :id="`${label}_text`" :disabled="insensitive" @blur="handleBlur"/>
6+
<span v-if="showWarning" class="warning-icon" :title="warningMessage">⚠️</span>
7+
</div>
8+
</div>
9+
</template>
10+
11+
<script setup lang="ts">
12+
13+
import { computed, ref } from 'vue';
14+
import InputText from 'primevue/inputtext';
15+
import SrLabelInfoIconButton from './SrLabelInfoIconButton.vue';
16+
17+
const props = defineProps({
18+
modelValue: {
19+
type: String,
20+
required: true
21+
},
22+
label: {
23+
type: String,
24+
default: 'Label'
25+
},
26+
insensitive: {
27+
type: Boolean,
28+
default: false
29+
},
30+
tooltipText: {
31+
type: String,
32+
default: 'tooltip text'
33+
},
34+
tooltipUrl: {
35+
type: String,
36+
default: ''
37+
},
38+
labelFontSize: {
39+
type: String,
40+
default: 'small' // default font size if not passed
41+
},
42+
});
43+
44+
const emit = defineEmits(['update:modelValue']);
45+
46+
const showWarning = ref(false);
47+
const warningMessage = ref('');
48+
49+
/**
50+
* Sanitize input for SQL field names
51+
* - Allow only alphanumeric characters and underscores
52+
* - Convert dashes to underscores
53+
* - Remove other special characters
54+
* - Ensure it starts with a letter or underscore
55+
*/
56+
function sanitizeSqlFieldName(value: string): string {
57+
if (!value) return value;
58+
59+
// Replace dashes with underscores
60+
let sanitized = value.replace(/-/g, '_');
61+
62+
// Remove any characters that are not alphanumeric or underscore
63+
sanitized = sanitized.replace(/[^a-zA-Z0-9_]/g, '');
64+
65+
// If it starts with a number, prepend an underscore
66+
if (/^\d/.test(sanitized)) {
67+
sanitized = '_' + sanitized;
68+
}
69+
70+
return sanitized;
71+
}
72+
73+
/**
74+
* Check if the value needs sanitization
75+
*/
76+
function needsSanitization(value: string): boolean {
77+
if (!value) return false;
78+
79+
// Check for invalid characters for SQL field names
80+
if (/[^a-zA-Z0-9_]/.test(value)) {
81+
return true;
82+
}
83+
84+
// Check if starts with a number
85+
if (/^\d/.test(value)) {
86+
return true;
87+
}
88+
89+
return false;
90+
}
91+
92+
// Create a computed with both getter and setter for modelValue
93+
const modelValueComputed = computed({
94+
get: () => props.modelValue, // Getter simply returns the current prop value
95+
set: (newValue) => {
96+
// Check if sanitization is needed
97+
if (needsSanitization(newValue)) {
98+
showWarning.value = true;
99+
warningMessage.value = 'Field name contains invalid characters for SQL. Will be sanitized on blur.';
100+
} else {
101+
showWarning.value = false;
102+
warningMessage.value = '';
103+
}
104+
emit('update:modelValue', newValue); // Emit the update event with the new value
105+
}
106+
});
107+
108+
// Handle blur event to sanitize the input
109+
function handleBlur() {
110+
if (needsSanitization(props.modelValue)) {
111+
const sanitized = sanitizeSqlFieldName(props.modelValue);
112+
emit('update:modelValue', sanitized);
113+
showWarning.value = false;
114+
warningMessage.value = '';
115+
}
116+
}
117+
</script>
118+
119+
<style scoped>
120+
.sr-sql-field-input-wrapper {
121+
border: 1px solid transparent;
122+
border-top: 0.125rem solid transparent;
123+
border-radius: var(--p-border-radius);
124+
margin-top: 0.125rem;
125+
font-size: small;
126+
}
127+
128+
.sr-text-row {
129+
display: flex;
130+
justify-content: space-between;
131+
align-items: center;
132+
padding: 0.125rem;
133+
gap: 0.5rem;
134+
}
135+
136+
.sr-text-input-label {
137+
margin-right: 0.5rem;
138+
font-size: small;
139+
white-space: nowrap;
140+
}
141+
142+
.sr-text-input-label-insensitive {
143+
margin-right: 0.5rem;
144+
font-size: small;
145+
white-space: nowrap;
146+
color: #888; /* grey color */
147+
}
148+
149+
.input-text {
150+
width: 15em; /* Adjust as needed for 5 digits */
151+
text-align: right;
152+
padding: 0.25rem;
153+
}
154+
155+
.warning-icon {
156+
font-size: 1.2rem;
157+
color: #ff9800;
158+
cursor: help;
159+
animation: pulse 1.5s ease-in-out infinite;
160+
}
161+
162+
@keyframes pulse {
163+
0%, 100% {
164+
opacity: 1;
165+
}
166+
50% {
167+
opacity: 0.5;
168+
}
169+
}
170+
</style>

web-client/src/stores/srToastStore.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ export const useSrToastStore = defineStore('srToast', {
1414
},
1515
error(title: string = 'I am title', body: string = 'I am body', life?: number): void {
1616
app.config.globalProperties.$toast.add({
17-
severity: 'error',
18-
summary: title,
19-
detail: body,
20-
life: life ?? this.life // Use provided life or default to this.life
17+
severity: 'error',
18+
summary: title,
19+
detail: body,
20+
life: life ?? 0 // Error toasts stay until user closes them (life: 0 means no auto-close)
2121
});
2222
},
2323
success(title: string = 'I am title', body: string = 'I am body', life?: number): void {

web-client/src/utils/SrMapUtils.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ import Overlay from 'ol/Overlay';
7373
import { extractSrRegionFromGeometry, processConvexHull } from '@/utils/geojsonUploader';
7474

7575

76-
// This grabs the constructor’s first parameter type
77-
type ScatterplotLayerProps = ConstructorParameters<typeof ScatterplotLayer>[0];
7876

7977
export const polyCoordsExist = computed(() => {
8078
let exist = false;
@@ -1462,23 +1460,26 @@ export const updateElevationMap = async (req_id: number) => {
14621460
analysisMapStore.analysisMapInitialized = true;
14631461
} else {
14641462
console.error(`updateElevationMap Failed to get firstRec for req_id:${req_id}`);
1465-
useSrToastStore().warn('No points in file', 'The request produced no points');
1463+
useSrToastStore().error('Failed to update map', 'Unable to retrieve first record from the request data');
14661464
}
14671465
} else {
14681466
console.warn(`updateElevationMap No points in file for req_id:${req_id}`);
1469-
useSrToastStore().warn('No points in file', 'The request produced no points');
1467+
useSrToastStore().error('Failed to update map', 'The request produced no elevation points');
14701468
}
14711469
} else {
14721470
console.error(`updateElevationMap Failed to get parms for req_id:${req_id}`);
1471+
useSrToastStore().error('Failed to update map', `Unable to retrieve parameters for request ID ${req_id}`);
14731472
}
14741473
} catch (error) {
1475-
console.warn('Failed to update selected request:', error);
1474+
console.error('Failed to update selected request:', error);
1475+
useSrToastStore().error('Failed to update map', `Error updating elevation data: ${error}`);
14761476
}
14771477
try {
14781478
await router.push(`/analyze/${useRecTreeStore().selectedReqId}`);
14791479
console.log('Successfully navigated to analyze:', useRecTreeStore().selectedReqId);
14801480
} catch (error) {
14811481
console.error('Failed to navigate to analyze:', error);
1482+
useSrToastStore().error('Navigation Error', `Failed to navigate to analyze: ${error}`);
14821483
}
14831484
const endTime = performance.now();
14841485
console.log(`updateElevationMap took ${endTime - startTime} milliseconds.`);
@@ -1496,12 +1497,15 @@ export async function updateMapAndPlot(reason:string): Promise<void> {
14961497
await callPlotUpdateDebounced(`updateMapAndPlot: ${reason}`);
14971498
} else {
14981499
console.error(`updateMapAndPlot Invalid req_id:${req_id}`);
1500+
useSrToastStore().error('Failed to update map and plot', `Invalid request ID: ${req_id}`);
14991501
}
15001502
} catch (error) {
15011503
if (error instanceof Error) {
15021504
console.error('updateMapAndPlot Failed:', error.message);
1505+
useSrToastStore().error('Failed to update map and plot', `${error.message}`);
15031506
} else {
15041507
console.error('updateMapAndPlot Unknown error occurred:', error);
1508+
useSrToastStore().error('Failed to update map and plot', `An unknown error occurred: ${error}`);
15051509
}
15061510
} finally {
15071511
const endTime = performance.now();

0 commit comments

Comments
 (0)