diff --git a/client/dive-common/apispec.ts b/client/dive-common/apispec.ts
index d8888c6b1..f47f05cc6 100644
--- a/client/dive-common/apispec.ts
+++ b/client/dive-common/apispec.ts
@@ -42,6 +42,8 @@ interface DiveParam {
type_props?: string[];
key: string;
default: string;
+ /** True if the user must supply a value before the pipeline can run. */
+ required?: boolean;
}
interface PipeMetadata {
diff --git a/client/dive-common/components/PipelineParamsDialog.vue b/client/dive-common/components/PipelineParamsDialog.vue
index 3585cbc03..f0c18566f 100644
--- a/client/dive-common/components/PipelineParamsDialog.vue
+++ b/client/dive-common/components/PipelineParamsDialog.vue
@@ -55,8 +55,14 @@ export default defineComponent({
strictlyPositive: (value: string | number) => Number(value) > 0 || 'Please enter a number > 0',
};
- function getRules(type: PipelineParamType): ValidationRule[] {
- const res = [];
+ function getRules(type: PipelineParamType, required = false): ValidationRule[] {
+ const res: ValidationRule[] = [];
+ if (required) {
+ res.push((value) => {
+ if (value === undefined || value === null || value === '') return 'Required';
+ return true;
+ });
+ }
if (type.includes('int')) {
res.push(rules.integer);
}
@@ -131,7 +137,7 @@ export default defineComponent({
:for="`input-${param.key}`"
class="text-caption font-weight-bold text-uppercase text--secondary"
>
- {{ param.label }}
+ {{ param.label }} *
@@ -157,7 +163,7 @@ export default defineComponent({
hide-details="auto"
class="mt-1"
:min="param.type === 'int' ? 'none' : 0"
- :rules="getRules(param.type)"
+ :rules="getRules(param.type, param.required)"
/>
@@ -171,7 +177,7 @@ export default defineComponent({
hide-details="auto"
class="mt-1"
:min="param.type === 'float' ? 'none' : 0"
- :rules="getRules(param.type)"
+ :rules="getRules(param.type, param.required)"
/>
@@ -193,7 +199,7 @@ export default defineComponent({
:min="param.type_props?.at(0) || 0"
:max="param.type_props?.at(1) || 100"
:step="param.type_props?.at(2) || 1"
- :rules="getRules(param.type)"
+ :rules="getRules(param.type, param.required)"
outlined
hide-details
/>
@@ -211,6 +217,7 @@ export default defineComponent({
dense
hide-details="auto"
class="mt-1"
+ :rules="getRules(param.type, param.required)"
/>
diff --git a/client/platform/desktop/backend/native/common.ts b/client/platform/desktop/backend/native/common.ts
index 3068bc910..ed5563ace 100644
--- a/client/platform/desktop/backend/native/common.ts
+++ b/client/platform/desktop/backend/native/common.ts
@@ -109,19 +109,38 @@ async function extractPipeMetadata(filePath: string): Promise {
const [, label, rawArgs] = diveMatch;
const args = rawArgs.split(',').map((arg) => arg.trim());
const type: PipelineParamType = args[0] as PipelineParamType;
- const pipelineTypeArgs = args.slice(1);
+ const restArgs = args.slice(1);
+ // `required` is a flag keyword — strip it from type_props,
+ // everything else stays positional for the type.
+ const isRequired = restArgs.some((a) => a.toLowerCase() === 'required');
+ const pipelineTypeArgs = restArgs.filter((a) => a.toLowerCase() !== 'required');
+
+ // `config = ` — absolute kwiver key, no process/block prefix
+ // applied. Used for global / cross-referenced settings.
+ const configMatch = trimmed.match(/^config\s+([\w:.-]+)\s*=\s*([^#]+)/i);
+ // Otherwise a regular per-process/block parameter assignment.
+ const paramLineMatch = !configMatch
+ ? trimmed.match(/^(?:relativepath\s+)?(?::)?([\w:-]+)\s*=?\s*([^#]+)/i)
+ : null;
+
+ let fullKey: string | null = null;
+ let defaultValue: string | null = null;
+ if (configMatch) {
+ fullKey = configMatch[1];
+ defaultValue = configMatch[2].trim();
+ } else if (paramLineMatch) {
+ fullKey = [...contextStack, paramLineMatch[1]].join(':');
+ defaultValue = paramLineMatch[2].trim();
+ }
- const paramLineMatch = trimmed.match(/^(?:relativepath\s+)?(?::)?([\w:-]+)\s*=?\s*([^#]+)/i);
- if (paramLineMatch) {
- const localKey = paramLineMatch[1];
- const defaultValue = paramLineMatch[2].trim();
- const fullKey = [...contextStack, localKey].join(':');
+ if (fullKey !== null && defaultValue !== null) {
metadata.diveParams!.push({
label,
type,
type_props: pipelineTypeArgs,
key: fullKey,
default: defaultValue,
+ ...(isRequired ? { required: true } : {}),
});
}
}
diff --git a/server/dive_tasks/pipeline_discovery.py b/server/dive_tasks/pipeline_discovery.py
index 426682b2b..889877ab4 100644
--- a/server/dive_tasks/pipeline_discovery.py
+++ b/server/dive_tasks/pipeline_discovery.py
@@ -72,22 +72,43 @@ def extract_pipe_metadata(file_path: Path) -> PipeMetadata:
label, raw_args = dive_match.groups()
args = [arg.strip() for arg in raw_args.split(',')]
param_type = args[0]
- pipeline_type_args = args[1:]
+ rest_args = args[1:]
+ # `required` is a flag keyword — strip it from type_props,
+ # everything else stays positional for the type.
+ is_required = any(a.lower() == 'required' for a in rest_args)
+ pipeline_type_args = [a for a in rest_args if a.lower() != 'required']
+
+ # `config = ` — absolute kwiver key, no
+ # process/block prefix. Used for global / cross-referenced
+ # settings.
+ config_match = re.match(r'^config\s+([\w:.-]+)\s*=\s*([^#]+)', trimmed, re.IGNORECASE)
+ # Otherwise a regular per-process/block parameter assignment.
+ param_line_match = (
+ re.match(r'^(?:relativepath\s+)?(?::)?([\w:-]+)\s*=?\s*([^#]+)', trimmed, re.IGNORECASE)
+ if not config_match else None
+ )
- param_line_match = re.match(r'^(?:relativepath\s+)?(?::)?([\w:-]+)\s*=?\s*([^#]+)', trimmed,
- re.IGNORECASE)
- if param_line_match:
+ full_key = None
+ default_val = None
+ if config_match:
+ full_key = config_match.group(1)
+ default_val = config_match.group(2).strip()
+ elif param_line_match:
local_key = param_line_match.group(1)
default_val = param_line_match.group(2).strip()
full_key = ":".join(context_stack + [local_key])
- metadata["diveParams"].append({
+ if full_key is not None and default_val is not None:
+ param_dict = {
"label": label,
"type": param_type,
"type_props": pipeline_type_args,
"key": full_key,
- "default": default_val
- })
+ "default": default_val,
+ }
+ if is_required:
+ param_dict["required"] = True
+ metadata["diveParams"].append(param_dict)
# --- Description extraction (Multiline) ---
desc_start_match = re.match(r'^#\s*Description:\s*(.*)', line_raw, re.IGNORECASE)
diff --git a/server/dive_utils/types.py b/server/dive_utils/types.py
index e057a6e3a..7bc06cf77 100644
--- a/server/dive_utils/types.py
+++ b/server/dive_utils/types.py
@@ -59,12 +59,14 @@ class Config:
extra = 'forbid'
-class DiveParam(TypedDict):
+class DiveParam(TypedDict, total=False):
label: str
type: str
type_props: list[str]
key: str
default: str
+ # True if the pipeline can't run until the user supplies a value
+ required: bool
class PipeMetadata(TypedDict):