Skip to content
Open
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
77 changes: 37 additions & 40 deletions client/dive-common/components/RunPipelineToast.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import { Pipe } from 'dive-common/apispec';
import { parsePipelineDataTypes } from 'dive-common/pipelineDataTypes';

export default defineComponent({
name: 'RunPipelineToast',
Expand All @@ -13,41 +14,12 @@ export default defineComponent({
},

setup(props) {
function getPipelineTypeIcon(type: string | undefined): string {
if (!type) {
return '';
}

switch (type.toUpperCase()) {
case 'IMAGE':
return 'mdi-image';
case 'VIDEO':
return 'mdi-video-vintage';
case 'TEXT':
return 'mdi-format-text';
case 'BBOX':
return 'mdi-vector-square';
case 'FULL-SIZE-BBOX':
return 'mdi-square-outline';
case 'HEAD-TAIL':
return 'mdi-vector-line';
case 'MASK':
return 'mdi-vector-polygon';
case 'TRACK':
return 'mdi-gesture';
case 'CALIBRATION':
return 'mdi-checkerboard';
default:
return type;
}
}

const inputIcon = computed(() => getPipelineTypeIcon(props.pipeline.metadata?.inputType));
const outputIcon = computed(() => getPipelineTypeIcon(props.pipeline.metadata?.outputType));
const inputTypes = computed(() => parsePipelineDataTypes(props.pipeline.metadata?.inputType));
const outputTypes = computed(() => parsePipelineDataTypes(props.pipeline.metadata?.outputType));

return {
inputIcon,
outputIcon,
inputTypes,
outputTypes,
};
},
});
Expand All @@ -56,18 +28,36 @@ export default defineComponent({
<template>
<div>
<span>{{ pipeline.metadata?.description }}</span>
<div v-if="pipeline.metadata?.inputType && pipeline.metadata?.outputType" class="pipeline-type-indicators">
<v-icon>
{{ inputIcon }}
</v-icon>
<div v-if="inputTypes.length && outputTypes.length" class="pipeline-type-indicators">
<span
v-for="(item, index) in inputTypes"
:key="`in-${index}`"
class="pipeline-type"
:title="item.qualifier ? `${item.name} (${item.qualifier})` : item.name"
>
<v-icon v-if="item.icon">
{{ item.icon }}
</v-icon>
<span v-else>{{ item.name }}</span>
<small v-if="item.qualifier">{{ item.qualifier }}</small>
</span>

<v-icon>
mdi-arrow-right
</v-icon>

<v-icon>
{{ outputIcon }}
</v-icon>
<span
v-for="(item, index) in outputTypes"
:key="`out-${index}`"
class="pipeline-type"
:title="item.qualifier ? `${item.name} (${item.qualifier})` : item.name"
>
<v-icon v-if="item.icon">
{{ item.icon }}
</v-icon>
<span v-else>{{ item.name }}</span>
<small v-if="item.qualifier">{{ item.qualifier }}</small>
</span>
</div>
</div>
</template>
Expand All @@ -86,6 +76,13 @@ export default defineComponent({
padding: 10px 0;
display: flex;
justify-content: left;
align-items: center;
gap: 10px;
}

.pipeline-type {
display: inline-flex;
align-items: center;
gap: 2px;
}
</style>
32 changes: 32 additions & 0 deletions client/dive-common/pipelineDataTypes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { parsePipelineDataTypes } from './pipelineDataTypes';

describe('parsePipelineDataTypes', () => {
it('maps a single type to its icon', () => {
expect(parsePipelineDataTypes('BBOX')).toEqual([{ name: 'BBOX', qualifier: undefined, icon: 'mdi-vector-square' }]);
});

it('splits compound outputs into one icon each', () => {
expect(parsePipelineDataTypes('BBOX + POLYGON + KEYPOINTS').map((t) => t.icon))
.toEqual(['mdi-vector-square', 'mdi-vector-polygon', 'mdi-vector-point']);
expect(parsePipelineDataTypes('BBOX + MASK + HEAD-TAIL').map((t) => t.icon))
.toEqual(['mdi-vector-square', 'mdi-vector-polygon', 'mdi-vector-line']);
});

it('keeps parenthesised qualifiers off the icon lookup', () => {
expect(parsePipelineDataTypes('IMAGE (per camera), BBOX (per camera)')).toEqual([
{ name: 'IMAGE', qualifier: 'per camera', icon: 'mdi-image' },
{ name: 'BBOX', qualifier: 'per camera', icon: 'mdi-vector-square' },
]);
expect(parsePipelineDataTypes('IMAGE (EO, IR)')).toEqual([{ name: 'IMAGE', qualifier: 'EO, IR', icon: 'mdi-image' }]);
});

it('is case insensitive and leaves unknown types without an icon', () => {
expect(parsePipelineDataTypes('mask')[0].icon).toBe('mdi-vector-polygon');
expect(parsePipelineDataTypes('DEPTH-MAP')).toEqual([{ name: 'DEPTH-MAP', qualifier: undefined, icon: undefined }]);
});

it('returns nothing for a missing header', () => {
expect(parsePipelineDataTypes(undefined)).toEqual([]);
expect(parsePipelineDataTypes(' ')).toEqual([]);
});
});
57 changes: 57 additions & 0 deletions client/dive-common/pipelineDataTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* The `# Input:` / `# Output:` header of a pipe names one or more data types,
* joined by `+` or `,`, each optionally qualified in parentheses, e.g.
* `BBOX + MASK + HEAD-TAIL` or `IMAGE (per camera), BBOX (per camera)`.
*/
export interface PipelineDataType {
name: string;
qualifier?: string;
icon?: string;
}

const ICONS: Record<string, string> = {
IMAGE: 'mdi-image',
VIDEO: 'mdi-video-vintage',
TEXT: 'mdi-format-text',
BBOX: 'mdi-vector-square',
'FULL-SIZE-BBOX': 'mdi-square-outline',
'HEAD-TAIL': 'mdi-vector-line',
KEYPOINT: 'mdi-vector-point',
KEYPOINTS: 'mdi-vector-point',
MASK: 'mdi-vector-polygon',
POLYGON: 'mdi-vector-polygon',
POLYGONS: 'mdi-vector-polygon',
TRACK: 'mdi-gesture',
TRACKS: 'mdi-gesture',
CALIBRATION: 'mdi-checkerboard',
};

function splitOutsideParentheses(spec: string): string[] {
const parts: string[] = [];
let depth = 0;
let current = '';
Array.from(spec).forEach((char) => {
if (char === '(') depth += 1;
if (char === ')') depth = Math.max(0, depth - 1);
if (depth === 0 && (char === '+' || char === ',')) {
parts.push(current);
current = '';
} else {
current += char;
}
});
parts.push(current);
return parts.map((part) => part.trim()).filter((part) => part);
}

export function parsePipelineDataTypes(spec: string | undefined): PipelineDataType[] {
if (!spec) {
return [];
}
return splitOutsideParentheses(spec).map((part) => {
const match = part.match(/^([^(]*?)\s*(?:\(([^)]*)\))?$/);
const name = (match?.[1] || part).trim();
const qualifier = match?.[2]?.trim() || undefined;
return { name, qualifier, icon: ICONS[name.toUpperCase()] };
});
}
Loading