From 059e43c904026ed951e23813a7599b90adb46870 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 21 Oct 2024 16:00:46 -0700 Subject: [PATCH 001/108] WIP --- src/components/timeline/Row.svelte | 164 +++++++++--------- .../form/TimelineEditorLayerSection.svelte | 20 ++- .../timeline/form/TimelineEditorPanel.svelte | 2 +- src/stores/views.ts | 2 +- src/types/timeline.ts | 34 +++- src/utilities/timeline.test.ts | 86 ++++++++- src/utilities/timeline.ts | 138 ++++++++++++++- src/utilities/view.ts | 4 +- 8 files changed, 345 insertions(+), 105 deletions(-) diff --git a/src/components/timeline/Row.svelte b/src/components/timeline/Row.svelte index 8e07830199..44944c0629 100644 --- a/src/components/timeline/Row.svelte +++ b/src/components/timeline/Row.svelte @@ -15,6 +15,7 @@ externalSources, planDerivationGroupLinks, } from '../../stores/external-source'; + import { activityTypes } from '../../stores/plan'; import { externalResources, fetchingResourcesExternal, @@ -69,6 +70,7 @@ import { getDoyTime } from '../../utilities/time'; import { TimelineInteractionMode, + applyActivityLayerFilter, directiveInView, externalEventInView, generateDiscreteTreeUtil, @@ -342,8 +344,14 @@ // helper for hasExternalEventsLayer; counts how many external event types are associated with this row (if all layers have 0 event types, we // don't want to allocate any canvas space in the row for the layer) + + // TODO figure out what it means for new activities here $: associatedActivityTypes = activityLayers - .map(layer => (layer.filter.activity ? layer.filter.activity.types.length : 0)) + .map(layer => + layer.filter.activity + ? ((layer.filter.activity.dynamic_type_filters?.length || layer.filter.activity.static_types?.length) ?? 0) + : 0, + ) .reduce((currentSum, newValue) => currentSum + newValue, 0); $: associatedEventTypes = externalEventLayers .map(layer => (layer.filter.externalEvent ? layer.filter.externalEvent.event_types.length : 0)) @@ -434,33 +442,35 @@ let seenSpanIds: Record = {}; activityLayers.forEach(layer => { if (layer.filter && layer.filter.activity !== undefined) { - const types = layer.filter.activity.types || []; - types.forEach(type => { - const matchingDirectives = directivesByType[type]; - if (matchingDirectives) { - const uniqueDirectives: ActivityDirective[] = []; - matchingDirectives.forEach(directive => { - if (!seenDirectiveIds[directive.id]) { - updatedIdToColorMaps.directives[directive.id] = layer.activityColor; - seenDirectiveIds[directive.id] = true; - uniqueDirectives.push(directive); - } - }); - directives = directives.concat(uniqueDirectives); - } - const matchingSpans = spansByType[type]; - if (matchingSpans) { - const uniqueSpans: Span[] = []; - matchingSpans.forEach(span => { - if (!seenSpanIds[span.span_id]) { - updatedIdToColorMaps.spans[span.span_id] = layer.activityColor; - seenSpanIds[span.span_id] = true; - uniqueSpans.push(span); - } - }); - spans = spans.concat(uniqueSpans); - } - }); + const { directives: matchingDirectives } = applyActivityLayerFilter( + layer.filter.activity, + activityDirectives, + spans, + $activityTypes, + ); + if (matchingDirectives) { + const uniqueDirectives: ActivityDirective[] = []; + matchingDirectives.forEach(directive => { + if (!seenDirectiveIds[directive.id]) { + idToColorMaps.directives[directive.id] = layer.activityColor; + seenDirectiveIds[directive.id] = true; + uniqueDirectives.push(directive); + } + }); + directives = directives.concat(uniqueDirectives); + } + // const matchingSpans = spansByType[type]; + // if (matchingSpans) { + // const uniqueSpans: Span[] = []; + // matchingSpans.forEach(span => { + // if (!seenSpanIds[span.span_id]) { + // idToColorMaps.spans[span.span_id] = layer.activityColor; + // seenSpanIds[span.span_id] = true; + // uniqueSpans.push(span); + // } + // }); + // spans = spans.concat(uniqueSpans); + // } } }); directives.sort((a, b) => ((a.start_time_ms ?? 0) < (b.start_time_ms ?? 0) ? -1 : 1)); @@ -471,8 +481,8 @@ // regeneration upon viewTimeRange change when not in filterActivitiesByTime mode. filteredActivityDirectives = directives; filteredSpans = spans; - timeFilteredActivityDirectives = directives; // if not actively filtering by time - timeFilteredSpans = spans; // if not actively filtering by time + timeFilteredActivityDirectives = directives; + timeFilteredSpans = spans; } else { filteredActivityDirectives = []; filteredSpans = []; @@ -480,60 +490,54 @@ timeFilteredSpans = []; } } - } - if (hasExternalEventsLayer) { - let externalEventsFilteredByDG = []; - filteredExternalEvents = []; - - let filteredDerivationGroups = $planDerivationGroupLinks - .filter( - link => link.plan_id === plan?.id && !($derivationGroupVisibilityMap[link.derivation_group_name] ?? true), - ) - .map(link => link.derivation_group_name); - - // Apply filter for hiding derivation groups - externalEventsFilteredByDG = externalEvents.filter(ee => { - let derivationGroup = - $externalSources.find( - externalSource => - externalSource.derivation_group_name === ee.pkey.derivation_group_name && - externalSource.key === ee.pkey.source_key, - )?.derivation_group_name ?? undefined; - // the statement below says return true (keep) if the plan is not null and if the filter for this plan does not include this derivation group - return plan && derivationGroup ? !filteredDerivationGroups.includes(derivationGroup) : false; - }); - // Filter by external event type - const externalEventsByType = groupBy(externalEventsFilteredByDG, 'pkey.event_type_name'); - externalEventLayers.forEach(layer => { - if (layer.filter && layer.filter.externalEvent !== undefined) { - const event_types = layer.filter.externalEvent.event_types || []; - event_types.forEach(type => { - const matchingEvents = externalEventsByType[type]; - if (matchingEvents) { - matchingEvents.forEach( - event => - (updatedIdToColorMaps.external_events[getExternalEventRowId(event.pkey)] = layer.externalEventColor), - ); - filteredExternalEvents = filteredExternalEvents.concat(unique(matchingEvents)); - } - }); - } - }); - filteredExternalEvents.sort((a, b) => (a.start_ms < b.start_ms ? -1 : 1)); - timeFilteredExternalEvents = filteredExternalEvents; // if not actively filtering by time - } + if (hasExternalEventsLayer) { + let externalEventsFilteredByDG = []; + filteredExternalEvents = []; + + let filteredDerivationGroups = $planDerivationGroupLinks + .filter( + link => link.plan_id === plan?.id && !($derivationGroupVisibilityMap[link.derivation_group_name] ?? true), + ) + .map(link => link.derivation_group_name); + + // Apply filter for hiding derivation groups + externalEventsFilteredByDG = externalEvents.filter(ee => { + let derivationGroup = + $externalSources.find( + externalSource => + externalSource.derivation_group_name === ee.pkey.derivation_group_name && + externalSource.key === ee.pkey.source_key, + )?.derivation_group_name ?? undefined; + // the statement below says return true (keep) if the plan is not null and if the filter for this plan does not include this derivation group + return plan && derivationGroup ? !filteredDerivationGroups.includes(derivationGroup) : false; + }); + // Filter by external event type + const externalEventsByType = groupBy(externalEventsFilteredByDG, 'pkey.event_type_name'); + externalEventLayers.forEach(layer => { + if (layer.filter && layer.filter.externalEvent !== undefined) { + const event_types = layer.filter.externalEvent.event_types || []; + event_types.forEach(type => { + const matchingEvents = externalEventsByType[type]; + if (matchingEvents) { + matchingEvents.forEach( + event => + (updatedIdToColorMaps.external_events[getExternalEventRowId(event.pkey)] = + layer.externalEventColor), + ); + filteredExternalEvents = filteredExternalEvents.concat(unique(matchingEvents)); + } + }); + } + }); + filteredExternalEvents.sort((a, b) => (a.start_ms < b.start_ms ? -1 : 1)); - // we update idToColorMaps via reassignment instead of by mutation so that Svelte reacts to updates correctly - idToColorMaps = updatedIdToColorMaps; + timeFilteredExternalEvents = filteredExternalEvents; // if not actively filtering by time + } + } } - $: if ( - ((hasActivityLayer && filteredActivityDirectives && filteredSpans) || - (hasExternalEventsLayer && filteredExternalEvents)) && - viewTimeRange && - filterItemsByTime - ) { + $: if (hasActivityLayer && filterItemsByTime && filteredActivityDirectives && filteredSpans && viewTimeRange) { timeFilteredSpans = filteredSpans.filter(span => spanInView(span, viewTimeRange)); timeFilteredActivityDirectives = filteredActivityDirectives.filter(directive => { let inView = directiveInView(directive, viewTimeRange); diff --git a/src/components/timeline/form/TimelineEditorLayerSection.svelte b/src/components/timeline/form/TimelineEditorLayerSection.svelte index d37b5436e0..f7f5faa12d 100644 --- a/src/components/timeline/form/TimelineEditorLayerSection.svelte +++ b/src/components/timeline/form/TimelineEditorLayerSection.svelte @@ -46,22 +46,24 @@ } } - $: { - // getFilterValuesForLayer + $: filterValues = getFilterValuesForLayer(layer); + + function getFilterValuesForLayer(layer: Layer) { if (isActivityLayer(layer)) { const activityLayer = layer; - const activityTypes = activityLayer.filter?.activity?.types ?? []; - filterValues = [...activityTypes]; + const activityTypes = activityLayer.filter?.activity?.static_types ?? []; + return [...activityTypes]; + } else if (isLineLayer(layer) || isXRangeLayer(layer)) { + const resourceLayer = layer; + const resourceNames = resourceLayer.filter?.resource?.names ?? []; + return [...resourceNames]; } else if (isExternalEventLayer(layer)) { // NOTE: if a derivation group is disabled, this doesn't get invoked and does not update. however, on dissociation it does. const externalEventLayer = layer; const externalEventTypes = externalEventLayer.filter?.externalEvent?.event_types ?? []; - filterValues = [...externalEventTypes]; - } else if (isLineLayer(layer) || isXRangeLayer(layer)) { - const resourceLayer = layer; - const resourceNames = resourceLayer.filter?.resource?.names ?? []; - filterValues = [...resourceNames]; + return [...externalEventTypes]; } + return []; } function handleDeleteLayerFilterValue(value: string) { diff --git a/src/components/timeline/form/TimelineEditorPanel.svelte b/src/components/timeline/form/TimelineEditorPanel.svelte index c970accaef..fc435db586 100644 --- a/src/components/timeline/form/TimelineEditorPanel.svelte +++ b/src/components/timeline/form/TimelineEditorPanel.svelte @@ -330,7 +330,7 @@ filter: { ...currentLayer.filter, activity: { - types: values, + static_types: values, }, }, }; diff --git a/src/stores/views.ts b/src/stores/views.ts index 5012173739..57246212e0 100644 --- a/src/stores/views.ts +++ b/src/stores/views.ts @@ -576,7 +576,7 @@ export function getUpdatedLayerWithFilters( return { layer: createTimelineActivityLayer(timelines, { activityColor: getUniqueColorForActivityLayer(row), - filter: { activity: { types: itemNames } }, + filter: { activity: { static_types: itemNames } }, }), }; } else if (type === 'externalEvent') { diff --git a/src/types/timeline.ts b/src/types/timeline.ts index 4f2ce30b08..870fd7528f 100644 --- a/src/types/timeline.ts +++ b/src/types/timeline.ts @@ -31,12 +31,43 @@ export interface ExternalEventLayer extends Layer { } export type ActivityLayerFilter = { - types: string[]; + dynamic_type_filters?: ActivityLayerDynamicFilter>[]; + global_filters?: ActivityLayerDynamicFilter< + Pick + >[]; + static_types?: string[]; + type_subfilters?: Record[]>; }; export type ExternalEventLayerFilter = { event_types: string[]; }; +export enum ActivityLayerFilterField { + 'Type' = 'Type', + 'Subsystem' = 'Subsystem', + 'Tag' = 'Tag', + 'Parameter' = 'Parameter', + 'SchedulingGoalId' = 'SchedulingGoalId', +} + +export type ActivityLayerDynamicFilter = { + field: keyof T; + operator: FilterOperator; + value: string | string[] | number | number[]; +}; + +export type FilterOperator = + | 'equals' + | 'does not equal' + | 'includes' + | 'does not include' + | 'is one of' + | 'is not one of' + | 'greater than' + | 'less than' + | 'is within' + | 'is not within'; + export type AxisDomainFitMode = 'fitPlan' | 'fitTimeWindow' | 'manual'; export type Axis = { @@ -92,7 +123,6 @@ export type ChartType = 'activity' | 'line' | 'x-range' | 'externalEvent'; export interface Layer { chartType: ChartType; filter: { - // TODO refactor in next PR to a unified filter activity?: ActivityLayerFilter; externalEvent?: ExternalEventLayerFilter; resource?: ResourceLayerFilter; diff --git a/src/utilities/timeline.test.ts b/src/utilities/timeline.test.ts index 61852b39d4..2ef2f8d169 100644 --- a/src/utilities/timeline.test.ts +++ b/src/utilities/timeline.test.ts @@ -5,12 +5,15 @@ import { ViewLineLayerColorPresets, ViewXRangeLayerSchemePresets, } from '../constants/view'; -import type { ActivityDirective } from '../types/activity'; +import type { ActivityDirective, ActivityType } from '../types/activity'; import type { ExternalEvent } from '../types/external-event'; import type { Resource, ResourceType, Span, SpanUtilityMaps, SpansMap } from '../types/simulation'; -import type { DiscreteTreeNode, TimeRange, Timeline, XRangeLayer } from '../types/timeline'; +import type { Tag } from '../types/tags'; +import type { ActivityLayerFilter, DiscreteTreeNode, TimeRange, Timeline, XRangeLayer } from '../types/timeline'; import { createSpanUtilityMaps } from './activities'; +import { convertUTCToMs } from './time'; import { + applyActivityLayerFilter, createHorizontalGuide, createRow, createTimeline, @@ -37,7 +40,6 @@ import { paginateNodes, spanInView, } from './timeline'; -import { convertUTCToMs } from './time'; const testSpans: Span[] = [ generateSpan({ @@ -197,6 +199,10 @@ function generateActivityDirective(properties: Partial): Acti }; } +function generateTag(properties: Partial): Tag { + return { color: '#FFFFFF', created_at: '', id: -1, name: '', owner: '', ...properties }; +} + function generateSpan(properties: Partial): Span { return { attributes: { arguments: {}, computedAttributes: {} }, @@ -230,6 +236,30 @@ function generateExternalEvent(properties: Partial): ExternalEven }; } +function generateDirective(properties: Partial): ActivityDirective { + return { + anchor_id: null, + anchored_to_start: true, + applied_preset: null, + arguments: {}, + created_at: '2022-08-03T18:21:51', + created_by: 'admin', + id: 1, + last_modified_arguments_at: '2022-08-03T21:53:22', + last_modified_at: '2022-08-03T21:53:22', + last_modified_by: 'admin', + metadata: {}, + name: 'foo 1', + plan_id: 1, + source_scheduling_goal_id: null, + start_offset: '10:00:00', + start_time_ms: 1715731443696, + tags: [], + type: 'foo', + ...properties, + }; +} + test('createTimeline', () => { const timelines = generateTimelines(); expect(timelines[0].id).toBe(0); @@ -1235,3 +1265,53 @@ describe('getTimeRangeAroundTime', () => { expect(timeRange.end - timeRange.start).toBe(48 * hourInMs); }); }); + +test('applyActivityLayerFilter', () => { + const activityTypes: ActivityType[] = [ + { + computed_attributes_value_schema: { items: {}, type: 'struct' }, + name: 'foo', + parameters: {}, + required_parameters: [], + }, + { + computed_attributes_value_schema: { items: {}, type: 'struct' }, + name: 'bar', + parameters: {}, + required_parameters: [], + subsystem_tag: { + color: '#FFFFFF', + created_at: '2022-08-03T18:21:51', + id: 1, + name: 'subsystem 1', + owner: 'frog', + }, + }, + ]; + const tags: Tag[] = [generateTag({ id: 1 }), generateTag({ id: 2 })]; + const filter: ActivityLayerFilter = { + dynamic_type_filters: [ + { field: 'Type', operator: 'includes', value: 'oo' }, + // { field: 'Type', operator: 'includes', value: 't' }, + // { field: 'Type', operator: 'does not equal', value: 'bat' }, + // { field: 'Subsystem', operator: 'does not include', value: [1] }, + ], + // global_filters: [{ field: 'Tag', operator: 'includes', value: [1] }], + static_types: [], + type_subfilters: { + bat: [{ field: 'Tag', operator: 'includes', value: [1] }], + }, + }; + const directives: ActivityDirective[] = [ + generateDirective({ id: 1, type: 'foo' }), + generateDirective({ id: 2, type: 'foo' }), + generateDirective({ id: 3, type: 'bar' }), + generateDirective({ id: 4, tags: [{ tag: generateTag({ id: 1 }) }], type: 'bat' }), + generateDirective({ id: 5, tags: [{ tag: generateTag({ id: 2 }) }], type: 'bat' }), + generateDirective({ id: 6, tags: [{ tag: generateTag({ id: 1 }) }], type: 'bop' }), + ]; + + expect(applyActivityLayerFilter(filter, directives, [], activityTypes).directives.map(d => d.id)).to.deep.eq([ + 1, 2, 3, 4, + ]); +}); diff --git a/src/utilities/timeline.ts b/src/utilities/timeline.ts index f1e18201f7..fb9014f575 100644 --- a/src/utilities/timeline.ts +++ b/src/utilities/timeline.ts @@ -13,18 +13,21 @@ import { type CountableTimeInterval, type TimeInterval, } from 'd3-time'; -import { groupBy } from 'lodash-es'; +import { groupBy, isArray } from 'lodash-es'; import { ViewDefaultDiscreteOptions, ViewDiscreteLayerColorPresets, ViewLineLayerColorPresets, ViewXRangeLayerSchemePresets, } from '../constants/view'; -import type { ActivityDirective } from '../types/activity'; +import type { ActivityDirective, ActivityType } from '../types/activity'; import type { ExternalEvent } from '../types/external-event'; import type { Resource, ResourceType, ResourceValue, Span, SpanUtilityMaps, SpansMap } from '../types/simulation'; import type { ActivityLayer, + ActivityLayerDynamicFilter, + ActivityLayerFilter, + ActivityLayerFilterField, ActivityOptions, Axis, DiscreteTree, @@ -575,11 +578,7 @@ export function createTimelineActivityLayer(timelines: Timeline[], args: Partial return { activityColor: ViewDiscreteLayerColorPresets[0], chartType: 'activity', - filter: { - activity: { - types: [], - }, - }, + filter: { activity: {} }, id, name: '', yAxisId: null, @@ -1387,3 +1386,128 @@ export function paginateNodes( }); return paginateNodes(newNodes, activityOrEvent, parentId, discreteTreeExpansionMap, depth + 1); } + +export function applyActivityLayerFilter( + filter: ActivityLayerFilter, + directives: ActivityDirective[], + spans: Span[], + types: ActivityType[], +) { + // const directivesByType = groupBy(directives, 'type'); + // const spansByType = groupBy(spans, 'type'); + const staticTypeMap: Record = (filter.static_types || []).reduce((acc, cur) => { + acc[cur] = true; + return acc; + }, {}); + // TODO could be passed in to avoid recomputing this + const typeDefMap: Record = (types || []).reduce((acc, cur) => { + acc[cur.name] = cur; + return acc; + }, {}); + + let filteredDirectives = directives; + if (filter.static_types?.length || filter.dynamic_type_filters?.length) { + filteredDirectives = directives.filter(directive => { + let included = true; + + // Check to see if directive is included in static list + if (filter.static_types?.length) { + included = !!staticTypeMap[directive.type]; + } + + // Check if necessary to see if directive is included in dynamic list + if ((!filter.static_types?.length || !included) && filter.dynamic_type_filters?.length) { + included = directiveMatchesDynamicFilters(directive, filter.dynamic_type_filters, typeDefMap); + } + + // Apply global filters on top of the types + if (filter.global_filters?.length) { + included = directiveMatchesDynamicFilters(directive, filter.global_filters, typeDefMap); + } + + // Apply type specific filters if found + if (included && filter.type_subfilters && filter.type_subfilters[directive.type]) { + included = directiveMatchesDynamicFilters(directive, filter.type_subfilters[directive.type], typeDefMap); + } + return included; + }); + } + + console.log( + 'filteredDirectives :>> ', + filteredDirectives.map(x => x.id), + directives, + ); + return { directives: filteredDirectives, spans }; +} + +export function directiveMatchesDynamicFilters( + directive: ActivityDirective, + dynamicFilters: ActivityLayerDynamicFilter[], + activityTypeDefMap: Record, +): boolean { + return dynamicFilters.reduce((acc, curr) => { + let matches = false; + if (curr.field === 'Type') { + matches = matchesDynamicFilter(directive.type, curr.operator, curr.value); + } else if (curr.field === 'Subsystem') { + // Get subsystem tag for this directive + let subsystemTagId = -1; + const typeDef = activityTypeDefMap[directive.type]; + if (typeDef?.subsystem_tag?.id) { + subsystemTagId = typeDef.subsystem_tag.id; + } + matches = matchesDynamicFilter(subsystemTagId, curr.operator, curr.value); + } else if (curr.field === 'Tag') { + const ids = directive.tags.map(tag => tag.tag.id); + console.log('ids :>> ', ids); + matches = matchesDynamicFilter(ids, curr.operator, curr.value); + console.log('matches :>> ', matches); + } + return acc || matches; + }, false); +} + +export function matchesDynamicFilter( + itemValue: ActivityLayerDynamicFilter['value'], // the actual value + operator: ActivityLayerDynamicFilter['operator'], + filterValue: ActivityLayerDynamicFilter['value'], // the value(s) we're comparing against +) { + switch (operator) { + case 'equals': + return itemValue === filterValue; + case 'does not equal': + return itemValue !== filterValue; + case 'includes': + if (typeof filterValue === 'string' && typeof itemValue === 'string') { + return itemValue.indexOf(filterValue) > -1; + } else if (isArray(filterValue)) { + return !!(isArray(itemValue) ? itemValue : [itemValue]).find( + item => (filterValue as (typeof itemValue)[]).indexOf(item) > -1, + ); + } + return false; + case 'does not include': + console.log('here', filterValue, itemValue); + if (typeof filterValue === 'string' && typeof itemValue === 'string') { + return itemValue.indexOf(filterValue) < 0; + } else if (isArray(filterValue)) { + return !(isArray(itemValue) ? itemValue : [itemValue]).find( + item => (filterValue as (typeof itemValue)[]).indexOf(item) > -1, + ); + } + return false; + case 'is one of': + if (!isArray(filterValue)) { + return itemValue === filterValue; + } + return (filterValue as (typeof itemValue)[]).indexOf(itemValue) > -1; + case 'is not one of': + if (!isArray(filterValue)) { + return itemValue !== filterValue; + } + return (filterValue as (typeof itemValue)[]).indexOf(itemValue) < 0; + default: + return false; + } +} diff --git a/src/utilities/view.ts b/src/utilities/view.ts index d30e7da4d7..a19df12c35 100644 --- a/src/utilities/view.ts +++ b/src/utilities/view.ts @@ -22,14 +22,14 @@ export function generateDefaultView( externalEventTypes: ExternalEventType[] = [], ): View { const now = new Date().toISOString(); - const types: string[] = activityTypes.map(({ name }) => name); + // const types: string[] = activityTypes.map(({ name }) => name); const timeline = createTimeline([], { marginLeft: 250, marginRight: 30 }); const timelines = [timeline]; // Start with the activity row const activityLayer = createTimelineActivityLayer(timelines, { - filter: { activity: { types } }, + // filter: { activity: { dynamic_type_filters: [{ field: 'Type', operator: 'includes', value: 'Banana' }] } }, }); const activityRow = createRow(timelines, { autoAdjustHeight: true, From 2f2aae23e5f33e12c55e832bce649a12b16a7b08 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Wed, 30 Oct 2024 15:24:59 -0700 Subject: [PATCH 002/108] Fixes for spans --- src/components/timeline/Row.svelte | 63 ++++++++++--------- .../timeline/RowHeaderDiscreteTree.svelte | 2 +- src/stores/views.ts | 6 +- src/utilities/activities.ts | 2 +- src/utilities/timeline.ts | 56 +++++++++++------ 5 files changed, 75 insertions(+), 54 deletions(-) diff --git a/src/components/timeline/Row.svelte b/src/components/timeline/Row.svelte index 44944c0629..16faa7fee9 100644 --- a/src/components/timeline/Row.svelte +++ b/src/components/timeline/Row.svelte @@ -60,6 +60,7 @@ TimelineItemType, XAxisTick, } from '../../types/timeline'; + import { getAllSpansForActivityDirective } from '../../utilities/activities'; import effects from '../../utilities/effects'; import { getExternalEventRowId } from '../../utilities/externalEvents'; import { classNames, unique } from '../../utilities/generic'; @@ -349,7 +350,7 @@ $: associatedActivityTypes = activityLayers .map(layer => layer.filter.activity - ? ((layer.filter.activity.dynamic_type_filters?.length || layer.filter.activity.static_types?.length) ?? 0) + ? (layer.filter.activity.dynamic_type_filters?.length || layer.filter.activity.static_types?.length) ?? 0 : 0, ) .reduce((currentSum, newValue) => currentSum + newValue, 0); @@ -430,8 +431,6 @@ }; if (activityLayers && spansMap && activityDirectives) { let spansList = Object.values(spansMap); - const directivesByType = groupBy(activityDirectives, 'type'); - const spansByType = groupBy(spansList, 'type'); if (activityLayers.length) { let directives: ActivityDirective[] = []; let spans: Span[] = []; @@ -442,35 +441,41 @@ let seenSpanIds: Record = {}; activityLayers.forEach(layer => { if (layer.filter && layer.filter.activity !== undefined) { - const { directives: matchingDirectives } = applyActivityLayerFilter( + const { directives: matchingDirectives, spans: matchingSpans } = applyActivityLayerFilter( layer.filter.activity, activityDirectives, - spans, + spansList, $activityTypes, ); - if (matchingDirectives) { - const uniqueDirectives: ActivityDirective[] = []; - matchingDirectives.forEach(directive => { - if (!seenDirectiveIds[directive.id]) { - idToColorMaps.directives[directive.id] = layer.activityColor; - seenDirectiveIds[directive.id] = true; - uniqueDirectives.push(directive); - } - }); - directives = directives.concat(uniqueDirectives); - } - // const matchingSpans = spansByType[type]; - // if (matchingSpans) { - // const uniqueSpans: Span[] = []; - // matchingSpans.forEach(span => { - // if (!seenSpanIds[span.span_id]) { - // idToColorMaps.spans[span.span_id] = layer.activityColor; - // seenSpanIds[span.span_id] = true; - // uniqueSpans.push(span); - // } - // }); - // spans = spans.concat(uniqueSpans); - // } + const uniqueDirectives: ActivityDirective[] = []; + matchingDirectives.forEach(directive => { + if (!seenDirectiveIds[directive.id]) { + idToColorMaps.directives[directive.id] = layer.activityColor; + seenDirectiveIds[directive.id] = true; + uniqueDirectives.push(directive); + + // Gather spans for directive since we always show all spans for a directive + // TODO aplave does not know if this is a good move, needs investigation and potentially options + // exposed to the user re whether to apply filters to spans or always include spans + const childSpans = getAllSpansForActivityDirective(directive.id, spansMap, spanUtilityMaps); + childSpans.forEach(span => { + seenSpanIds[span.span_id] = true; + idToColorMaps.spans[span.span_id] = layer.activityColor; + }); + spans = spans.concat(childSpans); + } + }); + directives = directives.concat(uniqueDirectives); + + const uniqueSpans: Span[] = []; + matchingSpans.forEach(span => { + if (!seenSpanIds[span.span_id]) { + idToColorMaps.spans[span.span_id] = layer.activityColor; + seenSpanIds[span.span_id] = true; + uniqueSpans.push(span); + } + }); + spans = spans.concat(uniqueSpans); } }); directives.sort((a, b) => ((a.start_time_ms ?? 0) < (b.start_time_ms ?? 0) ? -1 : 1)); @@ -725,7 +730,7 @@ // Determine if the row will visualize all requested activities let activitiesInRow = new Set(); activityLayers.forEach(layer => { - const layerActivities = layer.filter.activity?.types ?? []; + const layerActivities = layer.filter.activity?.static_types ?? []; activitiesInRow = new Set([...activitiesInRow, ...layerActivities]); }); const missingActivity = (items as ActivityType[]).find(item => !activitiesInRow.has(item.name)); diff --git a/src/components/timeline/RowHeaderDiscreteTree.svelte b/src/components/timeline/RowHeaderDiscreteTree.svelte index 4193b04d84..60751fdfb2 100644 --- a/src/components/timeline/RowHeaderDiscreteTree.svelte +++ b/src/components/timeline/RowHeaderDiscreteTree.svelte @@ -218,7 +218,7 @@ dispatch('discrete-tree-node-change', node)} >
diff --git a/src/stores/views.ts b/src/stores/views.ts index 57246212e0..9541c30978 100644 --- a/src/stores/views.ts +++ b/src/stores/views.ts @@ -609,7 +609,7 @@ export function getUpdatedLayerWithFilters( // Otherwise augment the filter of the specified layer let prop: string = ''; if (type === 'activity') { - prop = 'types'; + prop = 'static_types'; } else if (type === 'externalEvent') { prop = 'event_types'; } else { @@ -619,8 +619,8 @@ export function getUpdatedLayerWithFilters( const existingFilter = layer.filter[typedType]; let existingFilterItems: string[] = []; - if (existingFilter && (existingFilter as ActivityLayerFilter).types) { - existingFilterItems = (existingFilter as ActivityLayerFilter).types; + if (existingFilter && (existingFilter as ActivityLayerFilter).static_types) { + existingFilterItems = (existingFilter as ActivityLayerFilter).static_types ?? []; } else if (existingFilter && (existingFilter as ResourceLayerFilter).names) { existingFilterItems = (existingFilter as ResourceLayerFilter).names; } else if (existingFilter && (existingFilter as ExternalEventLayerFilter).event_types) { diff --git a/src/utilities/activities.ts b/src/utilities/activities.ts index 46d8f4362f..cc6678b00c 100644 --- a/src/utilities/activities.ts +++ b/src/utilities/activities.ts @@ -82,7 +82,7 @@ export function getAllSpansForActivityDirective( } /** - * Returns thd children IDs of a span + * Returns the children IDs of a span */ export function getAllSpanChildrenIds(spanId: number, spanUtilityMaps: SpanUtilityMaps): number[] { const children = spanUtilityMaps.spanIdToChildIdsMap[spanId]; diff --git a/src/utilities/timeline.ts b/src/utilities/timeline.ts index fb9014f575..7bb34de18d 100644 --- a/src/utilities/timeline.ts +++ b/src/utilities/timeline.ts @@ -1393,8 +1393,6 @@ export function applyActivityLayerFilter( spans: Span[], types: ActivityType[], ) { - // const directivesByType = groupBy(directives, 'type'); - // const spansByType = groupBy(spans, 'type'); const staticTypeMap: Record = (filter.static_types || []).reduce((acc, cur) => { acc[cur] = true; return acc; @@ -1406,9 +1404,10 @@ export function applyActivityLayerFilter( }, {}); let filteredDirectives = directives; + let filteredSpans = spans; if (filter.static_types?.length || filter.dynamic_type_filters?.length) { filteredDirectives = directives.filter(directive => { - let included = true; + let included = false; // Check to see if directive is included in static list if (filter.static_types?.length) { @@ -1417,52 +1416,69 @@ export function applyActivityLayerFilter( // Check if necessary to see if directive is included in dynamic list if ((!filter.static_types?.length || !included) && filter.dynamic_type_filters?.length) { - included = directiveMatchesDynamicFilters(directive, filter.dynamic_type_filters, typeDefMap); + included = directiveOrSpanMatchesDynamicFilters(directive, filter.dynamic_type_filters, typeDefMap); } // Apply global filters on top of the types if (filter.global_filters?.length) { - included = directiveMatchesDynamicFilters(directive, filter.global_filters, typeDefMap); + included = directiveOrSpanMatchesDynamicFilters(directive, filter.global_filters, typeDefMap); } // Apply type specific filters if found if (included && filter.type_subfilters && filter.type_subfilters[directive.type]) { - included = directiveMatchesDynamicFilters(directive, filter.type_subfilters[directive.type], typeDefMap); + included = directiveOrSpanMatchesDynamicFilters(directive, filter.type_subfilters[directive.type], typeDefMap); + } + return included; + }); + filteredSpans = spans.filter(span => { + let included = false; + + // Check to see if span is included in static list + if (filter.static_types?.length) { + included = !!staticTypeMap[span.type]; + } + + // Check if necessary to see if span is included in dynamic list + if ((!filter.static_types?.length || !included) && filter.dynamic_type_filters?.length) { + included = directiveOrSpanMatchesDynamicFilters(span, filter.dynamic_type_filters, typeDefMap); + } + + // Apply global filters on top of the types + if (filter.global_filters?.length) { + included = directiveOrSpanMatchesDynamicFilters(span, filter.global_filters, typeDefMap); + } + + // Apply type specific filters if found + if (included && filter.type_subfilters && filter.type_subfilters[span.type]) { + included = directiveOrSpanMatchesDynamicFilters(span, filter.type_subfilters[span.type], typeDefMap); } return included; }); } - console.log( - 'filteredDirectives :>> ', - filteredDirectives.map(x => x.id), - directives, - ); - return { directives: filteredDirectives, spans }; + return { directives: filteredDirectives, spans: filteredSpans }; } -export function directiveMatchesDynamicFilters( - directive: ActivityDirective, +export function directiveOrSpanMatchesDynamicFilters( + directiveOrSpan: ActivityDirective | Span, dynamicFilters: ActivityLayerDynamicFilter[], activityTypeDefMap: Record, ): boolean { return dynamicFilters.reduce((acc, curr) => { let matches = false; if (curr.field === 'Type') { - matches = matchesDynamicFilter(directive.type, curr.operator, curr.value); + matches = matchesDynamicFilter(directiveOrSpan.type, curr.operator, curr.value); } else if (curr.field === 'Subsystem') { // Get subsystem tag for this directive let subsystemTagId = -1; - const typeDef = activityTypeDefMap[directive.type]; + const typeDef = activityTypeDefMap[directiveOrSpan.type]; if (typeDef?.subsystem_tag?.id) { subsystemTagId = typeDef.subsystem_tag.id; } matches = matchesDynamicFilter(subsystemTagId, curr.operator, curr.value); - } else if (curr.field === 'Tag') { - const ids = directive.tags.map(tag => tag.tag.id); - console.log('ids :>> ', ids); + } else if (curr.field === 'Tag' && typeof isArray((directiveOrSpan as ActivityDirective).tags)) { + const ids = (directiveOrSpan as ActivityDirective).tags.map(tag => tag.tag.id); matches = matchesDynamicFilter(ids, curr.operator, curr.value); - console.log('matches :>> ', matches); } return acc || matches; }, false); From 3b160fbc918e856d126606738a359015a132cead Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Thu, 31 Oct 2024 09:01:26 -0700 Subject: [PATCH 003/108] Componentize editor section headers. Remove unused styles. --- .../form/TimelineEditor/EditorSection.svelte | 85 ++++++ .../form/TimelineEditorLayerSection.svelte | 65 ----- .../timeline/form/TimelineEditorPanel.svelte | 261 ++++++------------ 3 files changed, 170 insertions(+), 241 deletions(-) create mode 100644 src/components/timeline/form/TimelineEditor/EditorSection.svelte diff --git a/src/components/timeline/form/TimelineEditor/EditorSection.svelte b/src/components/timeline/form/TimelineEditor/EditorSection.svelte new file mode 100644 index 0000000000..19c306f08d --- /dev/null +++ b/src/components/timeline/form/TimelineEditor/EditorSection.svelte @@ -0,0 +1,85 @@ + + + + +
+
+
{pluralizedItem}
+ {#if creatable} +
+ {#if typeof itemCount === 'number' && itemCount > 0} + + {/if} + +
+ {/if} +
+ + +
+ + diff --git a/src/components/timeline/form/TimelineEditorLayerSection.svelte b/src/components/timeline/form/TimelineEditorLayerSection.svelte index f7f5faa12d..70da53eca0 100644 --- a/src/components/timeline/form/TimelineEditorLayerSection.svelte +++ b/src/components/timeline/form/TimelineEditorLayerSection.svelte @@ -146,75 +146,10 @@
diff --git a/src/components/timeline/form/TimelineEditorLayerSection.svelte b/src/components/timeline/form/TimelineEditorLayerSection.svelte index 70da53eca0..c9de3baabb 100644 --- a/src/components/timeline/form/TimelineEditorLayerSection.svelte +++ b/src/components/timeline/form/TimelineEditorLayerSection.svelte @@ -55,8 +55,8 @@ return [...activityTypes]; } else if (isLineLayer(layer) || isXRangeLayer(layer)) { const resourceLayer = layer; - const resourceNames = resourceLayer.filter?.resource?.names ?? []; - return [...resourceNames]; + const resourceName = resourceLayer.filter?.resource ?? ''; + return [resourceName]; } else if (isExternalEventLayer(layer)) { // NOTE: if a derivation group is disabled, this doesn't get invoked and does not update. however, on dissociation it does. const externalEventLayer = layer; diff --git a/src/components/timeline/form/TimelineEditorPanel.svelte b/src/components/timeline/form/TimelineEditorPanel.svelte index a77819a023..2ff33d0b95 100644 --- a/src/components/timeline/form/TimelineEditorPanel.svelte +++ b/src/components/timeline/form/TimelineEditorPanel.svelte @@ -66,6 +66,7 @@ createTimelineXRangeLayer, createVerticalGuide, createYAxis, + getNextLayerID, isActivityLayer, isExternalEventLayer, isLineLayer, @@ -82,7 +83,7 @@ import RadioButton from '../../ui/RadioButtons/RadioButton.svelte'; import RadioButtons from '../../ui/RadioButtons/RadioButtons.svelte'; import EditorSection from './TimelineEditor/EditorSection.svelte'; - import TimelineEditorLayerSection from './TimelineEditorLayerSection.svelte'; + import TimelineLayerEditor from './TimelineEditor/TimelineLayerEditor.svelte'; import TimelineEditorYAxisSettings from './TimelineEditorYAxisSettings.svelte'; export let gridSection: ViewGridSection; @@ -90,6 +91,9 @@ let horizontalGuides: HorizontalGuide[] = []; let editorWidth: number; let layers: Layer[] = []; + let activityLayers: ActivityLayer[] = []; + let resourceLayers: (LineLayer | XRangeLayer)[] = []; + let externalEventLayers: ExternalEventLayer[] = []; let timelines: Timeline[] = []; let rowHasNonActivityChartLayer: boolean = false; let rows: Row[] = []; @@ -108,6 +112,20 @@ $: horizontalGuides = selectedRow?.horizontalGuides || []; $: yAxes = selectedRow?.yAxes || []; $: layers = selectedRow?.layers || []; + $: if (layers) { + activityLayers = []; + resourceLayers = []; + externalEventLayers = []; + layers.forEach(l => { + if (isActivityLayer(l)) { + activityLayers.push(l); + } else if (isLineLayer(l) || isXRangeLayer(l)) { + resourceLayers.push(l); + } else if (isExternalEventLayer(l)) { + externalEventLayers.push(l); + } + }); + } $: rowHasActivityLayer = !!selectedRow?.layers.find(isActivityLayer) || false; $: rowHasExternalEventLayer = selectedRow?.layers.find(isExternalEventLayer) || false; $: rowHasNonActivityChartLayer = @@ -175,8 +193,22 @@ viewUpdateRow('yAxes', filteredYAxes); } - function handleNewLayerClick() { - const layer = createTimelineActivityLayer(timelines); + // TODO move to a util? + function createTimelineLayer(chartType: Layer['chartType']): Layer { + switch (chartType) { + case 'line': + return createTimelineLineLayer(timelines, yAxes); + case 'x-range': + return createTimelineXRangeLayer(timelines, yAxes); + case 'externalEvent': + return createTimelineExternalEventLayer(timelines); + default: + return createTimelineActivityLayer(timelines); + } + } + + function handleNewLayerClick(chartType: Layer['chartType']) { + let layer = createTimelineLayer(chartType); layers = [...layers, layer]; viewUpdateRow('layers', layers); } @@ -190,6 +222,11 @@ viewUpdateRow('layers', filteredLayers); } + function handleDuplicateLayer(layer: Layer) { + const duplicatedLayer = { ...structuredClone(layer), id: getNextLayerID(timelines) }; + viewUpdateRow('layers', [...layers, duplicatedLayer]); + } + function handleOptionRadioChange(event: CustomEvent<{ id: RadioButtonId }>, name: keyof DiscreteOptions) { const { id } = event.detail; viewUpdateRow('discreteOptions', { ...discreteOptions, [name]: id }); @@ -350,9 +387,7 @@ ...currentLayer, filter: { ...currentLayer.filter, - resource: { - names: values, - }, + resource: values[0], }, }; return newLayer; @@ -413,6 +448,7 @@ } function handleUpdateLayerColor(value: string, layer: Layer) { + console.log('value :>> ', value); const newLayers = layers.map(l => { if (layer.id === l.id) { if (isActivityLayer(l)) { @@ -519,8 +555,7 @@ {#if !selectedTimeline}
No timeline selected
{:else} -
-
Margins
+
event.preventDefault()}> @@ -547,7 +582,7 @@ /> -
+ @@ -1125,36 +1158,75 @@ {/if} handleNewLayerClick('activity')} + on:removeAll={handleRemoveAllLayersClick} + > + {#if activityLayers.length} +
+ {#each activityLayers as layer (layer.id)} + handleUpdateLayerProperty('name', name, layer)} + on:colorChange={({ detail: { color } }) => handleUpdateLayerColor(color, layer)} + on:remove={() => handleDeleteLayerClick(layer)} + on:duplicate={() => handleDuplicateLayer(layer)} + /> + {/each} +
+ {/if} +
+ handleNewLayerClick('line')} on:removeAll={handleRemoveAllLayersClick} > - {#if layers.length} -
Global Filters -
Subsystem, tag, parameter, etc...
+
Tag, parameter, scheduling goal, etc...
-
+ {#if dirtyFilter.global_filters?.length} +
+
+ {#each dirtyFilter.global_filters as filter, i (filter.id)} + onDynamicFilterRemove('global_filters', filter)} + on:change={event => onDynamicFilterChange('global_filters', event)} + verb={i === 0 ? 'Where' : 'and'} + schema={{ + Parameter: { + subfields: parameterSubfields, + }, + Tag: { + does_not_include: { type: 'tag', values: $tags }, + includes: { type: 'tag', values: $tags }, + }, + }} + /> + {/each} +
+
+ {/if}
@@ -248,6 +382,7 @@ .filter-section-title { display: flex; gap: 8px; + user-select: none; } .filter-section-title .hint { @@ -265,7 +400,7 @@ .filters { display: flex; - flex: 60%; + flex: 70%; flex-direction: column; gap: 8px; overflow: auto; @@ -275,7 +410,7 @@ .resulting-types { background: white; display: flex; - flex: 40%; + flex: 30%; flex-direction: column; overflow: hidden; padding: 8px; @@ -324,6 +459,13 @@ max-height: 200px; overflow: auto; } + .dynamic-filter-content { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 200px; + overflow: auto; + } .search-icon { align-items: center; diff --git a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte new file mode 100644 index 0000000000..9e8d420886 --- /dev/null +++ b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte @@ -0,0 +1,180 @@ + + + + +
+
{verb}
+ + {#if currentField === 'Parameter' && subfields} + + {/if} + + {#if currentType === 'string'} + + {:else if currentType === 'int' || currentType === 'real'} + + {:else if currentType === 'variant'} + + {:else if currentType === 'tag'} + {@const currentValueTags = (Array.isArray(currentValue) ? currentValue : []).map(t => + currentValuePossibilities.find(v => v.id === t), + )} + + + {/if} + +
+ + diff --git a/src/components/timeline/form/TimelineEditorPanel.svelte b/src/components/timeline/form/TimelineEditorPanel.svelte index c93554284a..cbd4a7adc1 100644 --- a/src/components/timeline/form/TimelineEditorPanel.svelte +++ b/src/components/timeline/form/TimelineEditorPanel.svelte @@ -775,6 +775,19 @@
+
+ + + + +
event.preventDefault()}> diff --git a/src/types/timeline.ts b/src/types/timeline.ts index 5b4bdccdb7..162084304a 100644 --- a/src/types/timeline.ts +++ b/src/types/timeline.ts @@ -2,6 +2,7 @@ import type { Selection } from 'd3-selection'; import type { ActivityDirective, ActivityDirectiveId, ActivityType } from './activity'; import type { ConstraintResultWithName } from './constraint'; import type { ExternalEvent, ExternalEventId, ExternalEventType } from './external-event'; +import type { ValueSchema } from './schema'; import type { ResourceType, Span, SpanId } from './simulation'; export type DiscreteTree = DiscreteTreeNode[]; @@ -30,8 +31,21 @@ export interface ExternalEventLayer extends Layer { externalEventColor: string; } +export enum ActivityLayerFilterField { + 'Type' = 'Type', + 'Name' = 'Name', + 'Subsystem' = 'Subsystem', + 'Tag' = 'Tag', + 'Parameter' = 'Parameter', + 'SchedulingGoalId' = 'SchedulingGoalId', +} + +export type DynamicFilterDataType = ValueSchema['type'] | 'tag'; + export type ActivityLayerFilter = { - dynamic_type_filters?: ActivityLayerDynamicFilter>[]; + dynamic_type_filters?: ActivityLayerDynamicFilter< + Pick + >[]; global_filters?: ActivityLayerDynamicFilter< Pick >[]; @@ -42,31 +56,26 @@ export type ExternalEventLayerFilter = { event_types: string[]; }; -export enum ActivityLayerFilterField { - 'Type' = 'Type', - 'Subsystem' = 'Subsystem', - 'Tag' = 'Tag', - 'Parameter' = 'Parameter', - 'SchedulingGoalId' = 'SchedulingGoalId', -} - export type ActivityLayerDynamicFilter = { field: keyof T; - operator: FilterOperator; + id: number; + operator: keyof typeof FilterOperator; + subfield?: { name: string; type: DynamicFilterDataType }; value: string | string[] | number | number[]; }; -export type FilterOperator = - | 'equals' - | 'does not equal' - | 'includes' - | 'does not include' - | 'is one of' - | 'is not one of' - | 'greater than' - | 'less than' - | 'is within' - | 'is not within'; +export enum FilterOperator { + 'equals' = 'equals', + 'does_not_equal' = 'does not equal', + 'includes' = 'includes', + 'does_not_include' = 'does not include', + 'is_one_of' = 'is one of', + 'is_not_one_of' = 'is not one of', + 'greater_than' = 'greater than', + 'less_than' = 'less than', + 'is_within' = 'is within', + 'is_not_within' = 'is not within', +} export type AxisDomainFitMode = 'fitPlan' | 'fitTimeWindow' | 'manual'; diff --git a/src/utilities/timeline.ts b/src/utilities/timeline.ts index b7a93db1e9..c733305644 100644 --- a/src/utilities/timeline.ts +++ b/src/utilities/timeline.ts @@ -22,6 +22,7 @@ import { } from '../constants/view'; import type { ActivityDirective, ActivityType } from '../types/activity'; import type { ExternalEvent } from '../types/external-event'; +import type { DefaultEffectiveArgumentsMap } from '../types/parameter'; import type { Resource, ResourceType, ResourceValue, Span, SpanUtilityMaps, SpansMap } from '../types/simulation'; import type { ActivityLayer, @@ -1384,6 +1385,7 @@ export function applyActivityLayerFilter( directives: ActivityDirective[], spans: Span[], types: ActivityType[], + defaultArgumentsMap: DefaultEffectiveArgumentsMap, ) { if (!filter) { return { directives, spans }; @@ -1399,6 +1401,7 @@ export function applyActivityLayerFilter( return acc; }, {}); + const anyTypeFiltersSpecified = !!(filter.static_types?.length || filter.dynamic_type_filters?.length); const filteredDirectives = directives.filter(directive => { let included = false; @@ -1409,17 +1412,29 @@ export function applyActivityLayerFilter( // Check if necessary to see if directive is included in dynamic list if ((!filter.static_types?.length || !included) && filter.dynamic_type_filters?.length) { - included = directiveOrSpanMatchesDynamicFilters(directive, filter.dynamic_type_filters, typeDefMap); + included = directiveOrSpanMatchesDynamicFilters( + directive, + filter.dynamic_type_filters, + typeDefMap, + defaultArgumentsMap, + ); } // Apply global filters on top of the types if (filter.global_filters?.length) { - included = directiveOrSpanMatchesDynamicFilters(directive, filter.global_filters, typeDefMap); + included = + directiveOrSpanMatchesDynamicFilters(directive, filter.global_filters, typeDefMap, defaultArgumentsMap) && + (anyTypeFiltersSpecified ? included : true); } // Apply type specific filters if found if (included && filter.type_subfilters && filter.type_subfilters[directive.type]) { - included = directiveOrSpanMatchesDynamicFilters(directive, filter.type_subfilters[directive.type], typeDefMap); + included = directiveOrSpanMatchesDynamicFilters( + directive, + filter.type_subfilters[directive.type], + typeDefMap, + defaultArgumentsMap, + ); } return included; }); @@ -1434,17 +1449,29 @@ export function applyActivityLayerFilter( // Check if necessary to see if span is included in dynamic list if ((!filter.static_types?.length || !included) && filter.dynamic_type_filters?.length) { - included = directiveOrSpanMatchesDynamicFilters(span, filter.dynamic_type_filters, typeDefMap); + included = directiveOrSpanMatchesDynamicFilters( + span, + filter.dynamic_type_filters, + typeDefMap, + defaultArgumentsMap, + ); } // Apply global filters on top of the types if (filter.global_filters?.length) { - included = directiveOrSpanMatchesDynamicFilters(span, filter.global_filters, typeDefMap); + included = + directiveOrSpanMatchesDynamicFilters(span, filter.global_filters, typeDefMap, defaultArgumentsMap) && + (anyTypeFiltersSpecified ? included : true); } // Apply type specific filters if found if (included && filter.type_subfilters && filter.type_subfilters[span.type]) { - included = directiveOrSpanMatchesDynamicFilters(span, filter.type_subfilters[span.type], typeDefMap); + included = directiveOrSpanMatchesDynamicFilters( + span, + filter.type_subfilters[span.type], + typeDefMap, + defaultArgumentsMap, + ); } return included; }); @@ -1484,11 +1511,14 @@ export function directiveOrSpanMatchesDynamicFilters( directiveOrSpan: ActivityDirective | Span, dynamicFilters: ActivityLayerDynamicFilter[], activityTypeDefMap: Record, + defaultArgumentsMap: DefaultEffectiveArgumentsMap, ): boolean { return dynamicFilters.reduce((acc, curr) => { let matches = false; if (curr.field === 'Type') { matches = matchesDynamicFilter(directiveOrSpan.type, curr.operator, curr.value); + } else if (curr.field === 'Name') { + matches = matchesDynamicFilter((directiveOrSpan as ActivityDirective).name, curr.operator, curr.value); } else if (curr.field === 'Subsystem') { // Get subsystem tag for this directive let subsystemTagId = -1; @@ -1497,12 +1527,27 @@ export function directiveOrSpanMatchesDynamicFilters( subsystemTagId = typeDef.subsystem_tag.id; } matches = matchesDynamicFilter(subsystemTagId, curr.operator, curr.value); - } else if (curr.field === 'Tag' && typeof isArray((directiveOrSpan as ActivityDirective).tags)) { + } else if (curr.field === 'Tag' && isArray((directiveOrSpan as ActivityDirective).tags)) { const ids = (directiveOrSpan as ActivityDirective).tags.map(tag => tag.tag.id); matches = matchesDynamicFilter(ids, curr.operator, curr.value); + } else if (curr.field === 'Parameter' && curr.subfield) { + const subfield = curr.subfield; + const args = (directiveOrSpan as ActivityDirective).arguments || (directiveOrSpan as Span).attributes.arguments; + let argument = args[subfield.name]; + if (argument === undefined) { + const isSpan = (directiveOrSpan as Span).span_id !== undefined; + if (!isSpan) { + // Get default + const defaultArgsForType = defaultArgumentsMap[directiveOrSpan.type]; + if (defaultArgsForType) { + argument = defaultArgsForType[subfield.name]; + } + } + } + matches = matchesDynamicFilter(argument, curr.operator, curr.value); } - return acc || matches; - }, false); + return acc && matches; + }, true); } export function typeMatchesDynamicFilters( @@ -1516,22 +1561,31 @@ export function typeMatchesDynamicFilters( } else if (curr.field === 'Subsystem' && typeof type.subsystem_tag?.id === 'number') { matches = matchesDynamicFilter(type.subsystem_tag.id, curr.operator, curr.value); } - return acc || matches; - }, false); + return acc && matches; + }, true); +} + +export function lowercase(value: any) { + return typeof value === 'string' ? value.toLowerCase() : value; } export function matchesDynamicFilter( - itemValue: ActivityLayerDynamicFilter['value'], // the actual value + rawItemValue: ActivityLayerDynamicFilter['value'], // the actual value operator: ActivityLayerDynamicFilter['operator'], - filterValue: ActivityLayerDynamicFilter['value'], // the value(s) we're comparing against + rawFilterValue: ActivityLayerDynamicFilter['value'], // the value(s) we're comparing against ) { + const itemValue = lowercase(rawItemValue); + const filterValue = lowercase(rawFilterValue); switch (operator) { case 'equals': return itemValue === filterValue; - case 'does not equal': + case 'does_not_equal': return itemValue !== filterValue; case 'includes': if (typeof filterValue === 'string' && typeof itemValue === 'string') { + if (filterValue === '') { + return false; + } return itemValue.indexOf(filterValue) > -1; } else if (isArray(filterValue)) { return !!(isArray(itemValue) ? itemValue : [itemValue]).find( @@ -1539,8 +1593,7 @@ export function matchesDynamicFilter( ); } return false; - case 'does not include': - console.log('here', filterValue, itemValue); + case 'does_not_include': if (typeof filterValue === 'string' && typeof itemValue === 'string') { return itemValue.indexOf(filterValue) < 0; } else if (isArray(filterValue)) { @@ -1549,12 +1602,12 @@ export function matchesDynamicFilter( ); } return false; - case 'is one of': + case 'is_one_of': if (!isArray(filterValue)) { return itemValue === filterValue; } return (filterValue as (typeof itemValue)[]).indexOf(itemValue) > -1; - case 'is not one of': + case 'is_not_one_of': if (!isArray(filterValue)) { return itemValue !== filterValue; } From eadd04bff67facacf1f5ef8a91517cbb08eef0f1 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 11 Nov 2024 17:17:42 -0800 Subject: [PATCH 015/108] Basic manual type selection list filtering --- .../ActivityFilterBuilder.svelte | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte index 1c9b19f83a..ded65de3ed 100644 --- a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte +++ b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte @@ -11,7 +11,11 @@ import { tags } from '../../../../stores/tags'; import type { ActivityLayerDynamicFilter, ActivityLayerFilter } from '../../../../types/timeline'; import { compare } from '../../../../utilities/generic'; - import { applyActivityLayerFilter, getMatchingTypesForActivityLayerFilter } from '../../../../utilities/timeline'; + import { + applyActivityLayerFilter, + getMatchingTypesForActivityLayerFilter, + lowercase, + } from '../../../../utilities/timeline'; import { tooltip } from '../../../../utilities/tooltip'; import Input from '../../../form/Input.svelte'; import Menu from '../../../menus/Menu.svelte'; @@ -30,6 +34,7 @@ let manualMenu: Menu; let rootRef: HTMLDivElement; let manualInputRef: HTMLInputElement; + let manualInputValue: string = ''; let shown: boolean = false; const dispatch = createEventDispatcher<{ @@ -66,7 +71,7 @@ } function onAddAllManualTypes() { - dirtyFilter = { ...dirtyFilter, static_types: $activityTypes.map(t => t.name) }; + dirtyFilter = { ...dirtyFilter, static_types: filteredActivityTypes.map(t => t.name) }; dispatch('filterChange', { filter: dirtyFilter }); } @@ -150,6 +155,14 @@ $: parameterSubfields = Object.values(allParameterTypes).sort((a, b) => compare(a.label, b.label)); // TODO support key/value for values array + $: filteredActivityTypes = $activityTypes.filter(type => { + if (!manualInputValue) { + return true; + } + + return lowercase(type.name).indexOf(lowercase(manualInputValue)) > -1; + }); + $: if (manualInputOpen) { manualMenu?.show(); } else { @@ -195,7 +208,7 @@ bind:this={manualInputRef} class="st-input w-100 manual-types-filter-input" placeholder="Select types" - value="" + bind:value={manualInputValue} on:click={() => { requestAnimationFrame(() => { if (!manualInputOpen) { @@ -215,15 +228,26 @@ on:hide={() => (manualInputOpen = false)} >
- onAddAllManualTypes()}> -
Add All +
-
- {#each $activityTypes as type} - onManualTypeToggled(type.name)}> - -1} /> -
{type.name}
+ {#if filteredActivityTypes.length > 0} + onAddAllManualTypes()}> +
+ Add {filteredActivityTypes.length !== $activityTypes.length ? 'Matching' : 'All'} + +
- {/each} + {#each filteredActivityTypes as type} + onManualTypeToggled(type.name)}> + -1} + tabindex={-1} + style:pointer-events="none" + /> +
{type.name}
+
+ {/each} + {:else} + No activities matching your filter + {/if}
From 77c826f0ffca11376273f3ca19a4130eee0e0347 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 11 Nov 2024 17:28:22 -0800 Subject: [PATCH 016/108] add cssgrid to activity filter builder --- .../ActivityFilterBuilder.svelte | 325 +++++++++--------- 1 file changed, 169 insertions(+), 156 deletions(-) diff --git a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte index ded65de3ed..94d734a27a 100644 --- a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte +++ b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte @@ -21,6 +21,8 @@ import Menu from '../../../menus/Menu.svelte'; import MenuHeader from '../../../menus/MenuHeader.svelte'; import MenuItem from '../../../menus/MenuItem.svelte'; + import CssGrid from '../../../ui/CssGrid.svelte'; + import CssGridGutter from '../../../ui/CssGridGutter.svelte'; import ActivityTypeResult from './ActivityTypeResult.svelte'; import Draggable from './Draggable.svelte'; import DynamicFilter from './DynamicFilter.svelte'; @@ -193,172 +195,181 @@
-
-
-
- Manually Select Types - -
-
- -
- { - requestAnimationFrame(() => { - if (!manualInputOpen) { - manualInputOpen = true; - } - }); - }} - /> - - -
- (manualInputOpen = false)} + +
+
+
+ Manually Select Types +
+ +
- {#if dirtyFilter.static_types?.length} -
- {#each dirtyFilter.static_types as name} - onManualTypeToggled(name)} /> - {/each} +
+ +
+ { + requestAnimationFrame(() => { + if (!manualInputOpen) { + manualInputOpen = true; + } + }); + }} + /> + + +
+ (manualInputOpen = false)} + > +
+ {#if filteredActivityTypes.length > 0} + onAddAllManualTypes()}> +
+ Add {filteredActivityTypes.length !== $activityTypes.length ? 'Matching' : 'All'} + +
+
+ {#each filteredActivityTypes as type} + onManualTypeToggled(type.name)}> + -1} + tabindex={-1} + style:pointer-events="none" + /> +
{type.name}
+
+ {/each} + {:else} + No activities matching your filter + {/if} +
+
- {/if} -
-
-
-
-
- Dynamically Select Types -
Name includes...
+ {#if dirtyFilter.static_types?.length} +
+ {#each dirtyFilter.static_types as name} + onManualTypeToggled(name)} /> + {/each} +
+ {/if}
-
- {#if dirtyFilter.dynamic_type_filters?.length} -
-
- {#each dirtyFilter.dynamic_type_filters as filter, i (filter.id)} - onDynamicFilterRemove('dynamic_type_filters', filter)} - on:change={event => onDynamicFilterChange('dynamic_type_filters', event)} - verb={i === 0 ? 'Where' : 'and'} - schema={{ - /* TODO include only subsystem tags */ - Name: { - does_not_equal: { type: 'string' }, - does_not_include: { type: 'string' }, - equals: { type: 'variant', values: $activityTypes.map(type => type.name) }, - includes: { type: 'string' }, - }, - Subsystem: { - does_not_include: { type: 'tag', values: $tags }, - includes: { type: 'tag', values: $tags }, - }, - Type: { - does_not_equal: { type: 'variant', values: $activityTypes.map(type => type.name) }, - does_not_include: { type: 'string' }, - equals: { type: 'variant', values: $activityTypes.map(type => type.name) }, - includes: { type: 'string' }, - }, - }} - /> - {/each} +
+
+
+ Dynamically Select Types +
Name includes...
+
- {/if} -
-
-
-
- Global Filters -
Tag, parameter, scheduling goal, etc...
-
- + {#if dirtyFilter.dynamic_type_filters?.length} +
+
+ {#each dirtyFilter.dynamic_type_filters as filter, i (filter.id)} + onDynamicFilterRemove('dynamic_type_filters', filter)} + on:change={event => onDynamicFilterChange('dynamic_type_filters', event)} + verb={i === 0 ? 'Where' : 'and'} + schema={{ + /* TODO include only subsystem tags */ + Name: { + does_not_equal: { type: 'string' }, + does_not_include: { type: 'string' }, + equals: { type: 'variant', values: $activityTypes.map(type => type.name) }, + includes: { type: 'string' }, + }, + Subsystem: { + does_not_include: { type: 'tag', values: $tags }, + includes: { type: 'tag', values: $tags }, + }, + Type: { + does_not_equal: { type: 'variant', values: $activityTypes.map(type => type.name) }, + does_not_include: { type: 'string' }, + equals: { type: 'variant', values: $activityTypes.map(type => type.name) }, + includes: { type: 'string' }, + }, + }} + /> + {/each} +
+
+ {/if}
- {#if dirtyFilter.global_filters?.length} -
-
- {#each dirtyFilter.global_filters as filter, i (filter.id)} - onDynamicFilterRemove('global_filters', filter)} - on:change={event => onDynamicFilterChange('global_filters', event)} - verb={i === 0 ? 'Where' : 'and'} - schema={{ - Parameter: { - subfields: parameterSubfields, - }, - Tag: { - does_not_include: { type: 'tag', values: $tags }, - includes: { type: 'tag', values: $tags }, - }, - }} - /> - {/each} +
+
+
+ Global Filters +
Tag, parameter, scheduling goal, etc...
+
- {/if} + {#if dirtyFilter.global_filters?.length} +
+
+ {#each dirtyFilter.global_filters as filter, i (filter.id)} + onDynamicFilterRemove('global_filters', filter)} + on:change={event => onDynamicFilterChange('global_filters', event)} + verb={i === 0 ? 'Where' : 'and'} + schema={{ + Parameter: { + subfields: parameterSubfields, + }, + Tag: { + does_not_include: { type: 'tag', values: $tags }, + includes: { type: 'tag', values: $tags }, + }, + }} + /> + {/each} +
+
+ {/if} +
-
-
-
Resulting Types
- -
- - -
- {#each matchingTypes as type} - - {/each} + + + +
+
Resulting Types
+ +
+ + +
+ {#each matchingTypes as type} + + {/each} +
-
+
{/if} @@ -424,7 +435,6 @@ .filters { display: flex; - flex: 70%; flex-direction: column; gap: 8px; overflow: auto; @@ -434,7 +444,6 @@ .resulting-types { background: white; display: flex; - flex: 30%; flex-direction: column; overflow: hidden; padding: 8px; @@ -496,4 +505,8 @@ color: var(--st-gray-50); display: flex; } + + :global(.activity-filter-grid) { + width: 100%; + } From 171af592afe1f06a05a8b02e553e2b65b36466e0 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 11 Nov 2024 18:37:39 -0800 Subject: [PATCH 017/108] Bring back resource editing in old form for now --- .../form/TimelineEditorLayerFilter.svelte | 39 ++++++++++++------- .../form/TimelineEditorLayerSection.svelte | 5 +-- .../timeline/form/TimelineEditorPanel.svelte | 34 +++++++++++----- 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/components/timeline/form/TimelineEditorLayerFilter.svelte b/src/components/timeline/form/TimelineEditorLayerFilter.svelte index 6b00175185..b6eb6341e3 100644 --- a/src/components/timeline/form/TimelineEditorLayerFilter.svelte +++ b/src/components/timeline/form/TimelineEditorLayerFilter.svelte @@ -25,6 +25,7 @@ let menuTitle: string = ''; let selectedValuesMap: Record = {}; let searchText: string = ''; + let allowMultiple = layer.chartType !== 'line' && layer.chartType !== 'x-range'; $: if (layer) { selectedValuesMap = listToMap(values); @@ -77,10 +78,14 @@ function toggleItem(value: string) { let newValues = []; - if (selectedValuesMap[value]) { - newValues = values.filter(i => value !== i); + if (allowMultiple) { + if (selectedValuesMap[value]) { + newValues = values.filter(i => value !== i); + } else { + newValues = [...values, value]; + } } else { - newValues = [...values, value]; + newValues = [value]; } dispatch('change', { values: newValues }); } @@ -126,17 +131,23 @@
No items matching filter
{/if}
- + {#if allowMultiple} + + {/if}
diff --git a/src/components/timeline/form/TimelineEditorLayerSection.svelte b/src/components/timeline/form/TimelineEditorLayerSection.svelte index c9de3baabb..818047de28 100644 --- a/src/components/timeline/form/TimelineEditorLayerSection.svelte +++ b/src/components/timeline/form/TimelineEditorLayerSection.svelte @@ -55,8 +55,7 @@ return [...activityTypes]; } else if (isLineLayer(layer) || isXRangeLayer(layer)) { const resourceLayer = layer; - const resourceName = resourceLayer.filter?.resource ?? ''; - return [resourceName]; + return resourceLayer.filter?.resource ? [resourceLayer.filter?.resource] : []; } else if (isExternalEventLayer(layer)) { // NOTE: if a derivation group is disabled, this doesn't get invoked and does not update. however, on dissociation it does. const externalEventLayer = layer; @@ -89,7 +88,7 @@ value={layer.chartType} on:change={event => dispatch('handleUpdateLayerChartType', { value: getTarget(event).value })} > - + diff --git a/src/components/timeline/form/TimelineEditorPanel.svelte b/src/components/timeline/form/TimelineEditorPanel.svelte index cbd4a7adc1..927103b079 100644 --- a/src/components/timeline/form/TimelineEditorPanel.svelte +++ b/src/components/timeline/form/TimelineEditorPanel.svelte @@ -84,6 +84,7 @@ import RadioButtons from '../../ui/RadioButtons/RadioButtons.svelte'; import EditorSection from './TimelineEditor/EditorSection.svelte'; import TimelineLayerEditor from './TimelineEditor/TimelineLayerEditor.svelte'; + import TimelineEditorLayerSection from './TimelineEditorLayerSection.svelte'; import TimelineEditorYAxisSettings from './TimelineEditorYAxisSettings.svelte'; export let gridSection: ViewGridSection; @@ -209,6 +210,18 @@ function handleNewLayerClick(chartType: Layer['chartType']) { let layer = createTimelineLayer(chartType); + + // Assign yAxisId to existing value or new axis + if (chartType === 'line' || chartType === 'x-range') { + if (yAxes.length > 0) { + layer.yAxisId = yAxes[0].id; + } else { + handleNewYAxisClick(); + layer.yAxisId = yAxes[0].id; + } + } + + console.log('layer, values :>> ', layer); layers = [...layers, layer]; viewUpdateRow('layers', layers); } @@ -1203,22 +1216,23 @@
{#each resourceLayers as layer (layer.id)} - handleUpdateLayerProperty('name', name, layer)} on:colorChange={({ detail: { color } }) => handleUpdateLayerColor(color, layer)} on:remove={() => handleDeleteLayerClick(layer)} on:duplicate={() => handleDuplicateLayer(layer)} + /> --> + handleUpdateLayerChartType(event.detail.value, layer)} + on:handleUpdateLayerFilter={event => handleUpdateLayerFilter(event.detail.values, layer)} + on:handleUpdateLayerProperty={event => + handleUpdateLayerProperty(event.detail.name, event.detail.value, layer)} + on:handleUpdateLayerColorScheme={event => handleUpdateLayerColorScheme(event.detail.value, layer)} + on:handleDeleteLayerClick={() => handleDeleteLayerClick(layer)} + {layer} + {yAxes} /> - {/each}
{/if} From 2445ac7ab88df67884520f68af8bbf36267b1c8f Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 11 Nov 2024 18:53:37 -0800 Subject: [PATCH 018/108] Remove all layer fixes --- .../timeline/form/TimelineEditorPanel.svelte | 10 ++++---- src/utilities/effects.ts | 23 +++++++++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/components/timeline/form/TimelineEditorPanel.svelte b/src/components/timeline/form/TimelineEditorPanel.svelte index 927103b079..0a0c219259 100644 --- a/src/components/timeline/form/TimelineEditorPanel.svelte +++ b/src/components/timeline/form/TimelineEditorPanel.svelte @@ -226,8 +226,8 @@ viewUpdateRow('layers', layers); } - function handleRemoveAllLayersClick() { - effects.deleteTimelineLayers(); + function handleRemoveAllLayersClick(chartType: 'activity' | 'resource' | 'externalEvent') { + effects.deleteTimelineLayers(layers, chartType); } function handleDeleteLayerClick(layer: Layer) { @@ -1187,7 +1187,7 @@ item="Activity Layer" itemCount={activityLayers.length} on:create={() => handleNewLayerClick('activity')} - on:removeAll={handleRemoveAllLayersClick} + on:removeAll={() => handleRemoveAllLayersClick('activity')} > {#if activityLayers.length}
@@ -1210,7 +1210,7 @@ item="Resource Layer" itemCount={resourceLayers.length} on:create={() => handleNewLayerClick('line')} - on:removeAll={handleRemoveAllLayersClick} + on:removeAll={() => handleRemoveAllLayersClick('resource')} > {#if resourceLayers.length} @@ -1242,7 +1242,7 @@ item="Event Layer" itemCount={externalEventLayers.length} on:create={() => handleNewLayerClick('externalEvent')} - on:removeAll={handleRemoveAllLayersClick} + on:removeAll={() => handleRemoveAllLayersClick('externalEvent')} > {#if externalEventLayers.length}
diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index 18b2595914..60d5b83666 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -211,7 +211,7 @@ import type { TagsInsertInput, TagsSetInput, } from '../types/tags'; -import type { Row, Timeline } from '../types/timeline'; +import type { Layer, Row, Timeline } from '../types/timeline'; import type { View, ViewDefinition, ViewInsertInput, ViewSlim, ViewUpdateInput } from '../types/view'; import { ActivityDeletionAction } from './activities'; import { parseCdlDictionary, toAmpcsXml } from './codemirror/cdlDictionary'; @@ -3066,15 +3066,30 @@ const effects = { } }, - async deleteTimelineLayers(timelineId?: number | null, rowId?: number | null) { + async deleteTimelineLayers( + layers: Layer[], + chartType: 'activity' | 'resource' | 'externalEvent', + timelineId?: number | null, + rowId?: number | null, + ) { const { confirm } = await showConfirmModal( 'Delete', - `Are you sure you want to delete all layers in this row?`, + `Are you sure you want to delete all ${chartType} layers in this row?`, 'Delete Rows', true, ); if (confirm) { - viewUpdateRow('layers', [], timelineId, rowId); + const filteredLayers = layers.filter(l => { + if (chartType === 'activity') { + return l.chartType !== 'activity'; + } else if (chartType === 'externalEvent') { + return l.chartType !== 'externalEvent'; + } else if (chartType === 'resource') { + return l.chartType !== 'line' && l.chartType !== 'x-range'; + } + return true; + }); + viewUpdateRow('layers', filteredLayers, timelineId, rowId); } }, From 57bb8ae7a36a8255dc7f5206f95eb1ad9b2b6a12 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 11 Nov 2024 18:55:48 -0800 Subject: [PATCH 019/108] Style fix --- .../form/TimelineEditor/DynamicFilter.svelte | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte index 9e8d420886..37ee14d125 100644 --- a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte +++ b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte @@ -154,12 +154,14 @@ currentValuePossibilities.find(v => v.id === t), )} - +
+ +
{/if} @@ -180,4 +191,16 @@ display: flex; height: min-content; } + + .filter-active::after { + background: #2f80ed; + border-radius: 10px; + content: ' '; + height: 5px; + pointer-events: none; + position: absolute; + right: 0; + top: 2px; + width: 5px; + } From 011b4c9f7503aa19347e64a7f4b4d6100e63d6a1 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 11 Nov 2024 19:45:01 -0800 Subject: [PATCH 022/108] Show resulting type and instance counts --- .../ActivityFilterBuilder.svelte | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte index 89cfa9c289..60668cd579 100644 --- a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte +++ b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte @@ -5,9 +5,11 @@ import SearchIcon from '@nasa-jpl/stellar/icons/search.svg?component'; import { createEventDispatcher } from 'svelte'; import FilterWithPlusIcon from '../../../../assets/filter-with-plus.svg?component'; + import DirectiveIcon from '../../../../assets/timeline-directive.svg?component'; + import SpanIcon from '../../../../assets/timeline-span.svg?component'; import { activityArgumentDefaultsMap, activityDirectivesMap } from '../../../../stores/activities'; import { activityTypes } from '../../../../stores/plan'; - import { spans } from '../../../../stores/simulation'; + import { spans, spanUtilityMaps } from '../../../../stores/simulation'; import { tags } from '../../../../stores/tags'; import type { ActivityLayerDynamicFilter, ActivityLayerFilter } from '../../../../types/timeline'; import { compare } from '../../../../utilities/generic'; @@ -39,6 +41,7 @@ let manualInputRef: HTMLInputElement; let manualInputValue: string = ''; let shown: boolean = false; + let instanceCount: number = 0; const dispatch = createEventDispatcher<{ filterChange: { filter: ActivityLayerFilter }; @@ -131,9 +134,26 @@ $activityTypes, $activityArgumentDefaultsMap, ); - $: resultingTypes = new Set(appliedFilter.directives.map(d => d.type).concat(appliedFilter.spans.map(s => s.type))); + + $: if (appliedFilter) { + const seenSpans: Record = {}; + let count = appliedFilter.directives.length; + appliedFilter.directives.forEach(directive => { + const matchingSpanId = $spanUtilityMaps.directiveIdToSpanIdMap[directive.id]; + if (typeof matchingSpanId === 'number') { + seenSpans[matchingSpanId] = true; + } + }); + appliedFilter.spans.forEach(span => { + if (!seenSpans[span.span_id]) { + count++; + } + }); + instanceCount = count; + } + $: matchingTypes = getMatchingTypesForActivityLayerFilter(dirtyFilter, $activityTypes); - // TODO need to get the list of matching types and then grab the actual applied filter + // TODO need to get the list of matching types and then grab the actual applied filter? $: allParameterTypes = $activityTypes.reduce((acc, activityType) => { Object.entries(activityType.parameters).forEach(([parameterName, parameter]) => { const parameterType = parameter.schema.type; @@ -361,7 +381,13 @@
-
Resulting Types
+
+ Resulting Types +
+
{matchingTypes.length} types
+
{instanceCount} instances
+
+
@@ -458,9 +484,22 @@ .resulting-types-title { display: flex; + justify-content: space-between; padding-bottom: 8px; } + .resulting-types-info-container { + display: flex; + gap: 8px; + } + + .resulting-types-info { + color: var(--st-gray-50); + display: flex; + flex-direction: row; + gap: 4px; + } + .resulting-types-list { margin-top: 8px; overflow: auto; From 0f4425145a9cedfd8d78bc9dba29c0b5aee39eaf Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 11 Nov 2024 19:57:33 -0800 Subject: [PATCH 023/108] Greater/less than support --- src/utilities/timeline.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/utilities/timeline.ts b/src/utilities/timeline.ts index e237185f12..341479f1f7 100644 --- a/src/utilities/timeline.ts +++ b/src/utilities/timeline.ts @@ -1625,6 +1625,16 @@ export function matchesDynamicFilter( return itemValue !== filterValue; } return (filterValue as (typeof itemValue)[]).indexOf(itemValue) < 0; + case 'greater_than': + if (typeof filterValue === 'number' && typeof itemValue === 'number') { + return itemValue > filterValue; + } + return false; + case 'less_than': + if (typeof filterValue === 'number' && typeof itemValue === 'number') { + return itemValue < filterValue; + } + return false; default: return false; } From 7d1c57b820929c16a85a533c7f34009c998b386b Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 11 Nov 2024 19:57:38 -0800 Subject: [PATCH 024/108] Remove log --- src/components/timeline/form/TimelineEditorPanel.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/timeline/form/TimelineEditorPanel.svelte b/src/components/timeline/form/TimelineEditorPanel.svelte index 0a0c219259..adf87a93ab 100644 --- a/src/components/timeline/form/TimelineEditorPanel.svelte +++ b/src/components/timeline/form/TimelineEditorPanel.svelte @@ -221,7 +221,6 @@ } } - console.log('layer, values :>> ', layer); layers = [...layers, layer]; viewUpdateRow('layers', layers); } From 1daa49f4f96bfe1503520fcace9ea4c8f8eaea9a Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 11 Nov 2024 19:57:45 -0800 Subject: [PATCH 025/108] use > and < --- .../form/TimelineEditor/DynamicFilter.svelte | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte index 37ee14d125..f4979bfb15 100644 --- a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte +++ b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte @@ -36,7 +36,6 @@ let currentField = dirtyFilter.field as keyof typeof ActivityLayerFilterFieldType; let currentOperator: keyof typeof FilterOperator | null = dirtyFilter.operator; let subfields: SubfieldSchema[] | null = schema.Parameter?.subfields || null; - // let currentSubfield = dirtyFilter.subfield; let currentSubfieldLabel = dirtyFilter.field === 'Parameter' ? `${dirtyFilter.subfield?.name} (${dirtyFilter.subfield?.type})` : ''; let currentType: DynamicFilterDataType = 'string'; @@ -44,22 +43,7 @@ let operatorKeys: (keyof typeof FilterOperator)[] = []; let currentValuePossibilities: Array = []; - $: if (currentField === 'Parameter' && subfields) { - // let subfield = currentSubfield; - // let label = ''; - // if (!currentSubfieldLabel) { - // label = `${dirtyFilter.subfield?.name} (${dirtyFilter.subfield?.type})`; - // } else if (currentSubfield) { - // label = `${currentSubfield?.name} (${currentSubfield?.type})`; - // } - // const subfield = subfields.find(subfield => subfield.label === label); - // if (subfield) { - // // currentSubfield = subfield; - // currentSubfieldLabel = subfield.label; - // currentType = subfield.type; - // } - } else { - // currentSubfield = undefined; + $: if (currentField !== 'Parameter') { currentSubfieldLabel = ''; operatorKeys = Object.keys(schema[currentField]) as (keyof typeof FilterOperator)[]; currentType = (schema[currentField][currentOperator] || Object.values(schema[currentField])[0]).type; @@ -77,7 +61,7 @@ if (matchingSubfield.type === 'string') { operatorKeys = ['includes', 'does_not_include', 'equals', 'does_not_equal']; } else if (matchingSubfield.type === 'int' || matchingSubfield.type === 'real') { - operatorKeys = ['equals', 'does_not_equal']; + operatorKeys = ['equals', 'does_not_equal', 'greater_than', 'less_than']; } else if (matchingSubfield.type === 'variant') { operatorKeys = ['equals', 'does_not_equal']; currentValuePossibilities = matchingSubfield.values || []; From 05476c717754200b5bbc2a0fec5a5fa319f33025 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 11 Nov 2024 20:09:49 -0800 Subject: [PATCH 026/108] Reactivity fix --- src/components/timeline/Row.svelte | 1 + .../form/TimelineEditor/ActivityFilterBuilder.svelte | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/timeline/Row.svelte b/src/components/timeline/Row.svelte index c660f11a38..be18d2565b 100644 --- a/src/components/timeline/Row.svelte +++ b/src/components/timeline/Row.svelte @@ -735,6 +735,7 @@ // Determine if the row will visualize all requested activities let activitiesInRow = new Set(); activityLayers.forEach(layer => { + // TODO should we consider dynamic types here? Or just static? const layerActivities = layer.filter.activity?.static_types ?? []; activitiesInRow = new Set([...activitiesInRow, ...layerActivities]); }); diff --git a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte index 60668cd579..844392f2fa 100644 --- a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte +++ b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte @@ -32,9 +32,7 @@ export let filter: ActivityLayerFilter | undefined; - let dirtyFilter: ActivityLayerFilter = filter - ? structuredClone(filter) - : { dynamic_type_filters: [], global_filters: [], static_types: [] }; + let dirtyFilter: ActivityLayerFilter = { dynamic_type_filters: [], global_filters: [], static_types: [] }; let manualInputOpen: boolean = false; let manualMenu: Menu; let rootRef: HTMLDivElement; @@ -126,6 +124,10 @@ dispatch('filterChange', { filter: dirtyFilter }); } + $: if (filter) { + dirtyFilter = structuredClone(filter); + } + $: activityDirectives = Object.values($activityDirectivesMap); $: appliedFilter = applyActivityLayerFilter( dirtyFilter, From 524def483395310f45921df5a874bc8cb1d1199c Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Tue, 12 Nov 2024 13:09:44 -0800 Subject: [PATCH 027/108] Support boolean comparison --- .../timeline/form/TimelineEditor/DynamicFilter.svelte | 7 +++++++ src/types/timeline.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte index f4979bfb15..f2cf5ed25f 100644 --- a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte +++ b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte @@ -62,6 +62,8 @@ operatorKeys = ['includes', 'does_not_include', 'equals', 'does_not_equal']; } else if (matchingSubfield.type === 'int' || matchingSubfield.type === 'real') { operatorKeys = ['equals', 'does_not_equal', 'greater_than', 'less_than']; + } else if (matchingSubfield.type === 'boolean') { + operatorKeys = ['equals', 'does_not_equal']; } else if (matchingSubfield.type === 'variant') { operatorKeys = ['equals', 'does_not_equal']; currentValuePossibilities = matchingSubfield.values || []; @@ -127,6 +129,11 @@ {:else if currentType === 'int' || currentType === 'real'} + {:else if currentType === 'boolean'} + {:else if currentType === 'variant'}
- +
- {#each matchingTypes as type} + {#each filteredMatchingTypes as type} {/each} + {#if matchingTypes.length && !filteredMatchingTypes.length} +
No activities matching your filter
+ {/if}
From 673d0064872afec64d12b8315597406c58453182 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Thu, 21 Nov 2024 07:31:17 -0800 Subject: [PATCH 029/108] Bug fix --- .../form/TimelineEditor/DynamicFilter.svelte | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte index f2cf5ed25f..047f40c8c7 100644 --- a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte +++ b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte @@ -10,6 +10,7 @@ ActivityLayerFilterField as ActivityLayerFilterFieldType, FilterOperator, } from '../../../../types/timeline'; + import { getTarget } from '../../../../utilities/generic'; import TagsInput from '../../../ui/Tags/TagsInput.svelte'; type Subfield = { name: string; type: DynamicFilterDataType }; @@ -50,9 +51,9 @@ currentValuePossibilities = schema[currentField][currentOperator] ? schema[currentField][currentOperator].values : Object.values(schema[currentField])[0].values; - currentValue = ''; } + $: console.log('currentField :>> ', currentField, currentSubfieldLabel, subfields); $: if (currentField === 'Parameter' && currentSubfieldLabel !== undefined && subfields) { const matchingSubfield = subfields.find(subfield => subfield.label === currentSubfieldLabel) || subfields[0]; if (matchingSubfield) { @@ -101,14 +102,18 @@ } } - // function getSubfieldID(subfield: Subfield) { - // return `${subfield.name}____${subfield.type}`; - // } + function onFieldChange(event: Event) { + const { value } = getTarget(event); + if (value) { + currentValue = ''; + currentField = value as keyof typeof ActivityLayerFilterFieldType; + } + }
{verb}
- {#each Object.keys(schema) as key} {/each} From f433e7a853c1ff21e417243b7aa4094cc2e5f74c Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Thu, 21 Nov 2024 07:56:18 -0800 Subject: [PATCH 030/108] Support for min col and row css grid sizing --- .../form/TimelineEditor/ActivityFilterBuilder.svelte | 2 +- .../timeline/form/TimelineEditor/DynamicFilter.svelte | 2 ++ src/components/ui/CssGrid.svelte | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte index 3ae664ba14..e809f55099 100644 --- a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte +++ b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte @@ -229,7 +229,7 @@
- +
diff --git a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte index 047f40c8c7..2c25469340 100644 --- a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte +++ b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte @@ -105,6 +105,8 @@ function onFieldChange(event: Event) { const { value } = getTarget(event); if (value) { + // Since we changed the field we should reset the value + // TODO should we reset more bits than this? currentValue = ''; currentField = value as keyof typeof ActivityLayerFilterFieldType; } diff --git a/src/components/ui/CssGrid.svelte b/src/components/ui/CssGrid.svelte index 94af87807d..3704412d03 100644 --- a/src/components/ui/CssGrid.svelte +++ b/src/components/ui/CssGrid.svelte @@ -1,15 +1,17 @@
@@ -407,8 +423,8 @@ {#each filteredMatchingTypes as type} {/each} - {#if matchingTypes.length && !filteredMatchingTypes.length} -
No activities matching your filter
+ {#if resultingTypesMessage} +
{resultingTypesMessage}
{/if}
diff --git a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte index 498aed2b1c..50bb419521 100644 --- a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte +++ b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte @@ -54,7 +54,6 @@ : Object.values(schema[currentField])[0].values; } - $: console.log('currentField :>> ', currentField, currentSubfieldLabel, subfields); $: if (currentField === 'Parameter' && currentSubfieldLabel !== undefined && subfields) { const matchingSubfield = subfields.find(subfield => subfield.label === currentSubfieldLabel) || subfields[0]; if (matchingSubfield) { @@ -116,7 +115,7 @@
{verb}
- {#each Object.keys(schema) as key} {/each} From e3017f3464d48b077ea516c2fe013ca6704d2758 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Thu, 21 Nov 2024 13:28:38 -0800 Subject: [PATCH 034/108] Scheduling goal id filtering WIP support --- .../form/TimelineEditor/ActivityFilterBuilder.svelte | 4 ++++ .../timeline/form/TimelineEditor/DynamicFilter.svelte | 2 ++ src/utilities/timeline.ts | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte index 54d0f843e3..628e39bb42 100644 --- a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte +++ b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte @@ -392,6 +392,10 @@ Parameter: { subfields: parameterSubfields, }, + SchedulingGoalId: { + does_not_equal: { type: 'int' }, + equals: { type: 'int' }, + }, Tag: { does_not_include: { type: 'tag', values: $tags }, includes: { type: 'tag', values: $tags }, diff --git a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte index 50bb419521..9a8de4f66f 100644 --- a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte +++ b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte @@ -49,6 +49,8 @@ currentSubfieldLabel = ''; operatorKeys = Object.keys(schema[currentField]) as (keyof typeof FilterOperator)[]; currentType = (schema[currentField][currentOperator] || Object.values(schema[currentField])[0]).type; + // TODO filter to only the types included + // TODO value possibilities should be the union of all of the variants in case foo.A and bar.A have diff variants of the same type currentValuePossibilities = schema[currentField][currentOperator] ? schema[currentField][currentOperator].values : Object.values(schema[currentField])[0].values; diff --git a/src/utilities/timeline.ts b/src/utilities/timeline.ts index 341479f1f7..a4beffc462 100644 --- a/src/utilities/timeline.ts +++ b/src/utilities/timeline.ts @@ -1558,6 +1558,12 @@ export function directiveOrSpanMatchesDynamicFilters( } } matches = matchesDynamicFilter(argument, curr.operator, curr.value); + } else if (curr.field === 'SchedulingGoalId') { + // TODO need to test this once model is working + const goalId = (directiveOrSpan as ActivityDirective).source_scheduling_goal_id; + if (typeof goalId === 'number') { + matches = matchesDynamicFilter(goalId, curr.operator, curr.value); + } } return acc && matches; }, true); From f0aa1f88a0ed68afd4366d79a4e6b3013fbb6136 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Thu, 21 Nov 2024 13:44:53 -0800 Subject: [PATCH 035/108] Manual type selection menu positioning fix --- .../ActivityFilterBuilder.svelte | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte index 628e39bb42..74d89a13b2 100644 --- a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte +++ b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte @@ -38,6 +38,7 @@ let resultingTypesMessage: string = ''; let rootRef: HTMLDivElement; let manualInputRef: HTMLInputElement; + let manualInputWidth: number = 600; let manualInputValue: string = ''; let resultingTypesInputValue: string = ''; let shown: boolean = false; @@ -259,26 +260,27 @@
- -
- { - requestAnimationFrame(() => { - if (!manualInputOpen) { - manualInputOpen = true; - } - }); - }} - /> - - +
+ +
+ { + requestAnimationFrame(() => { + if (!manualInputOpen) { + manualInputOpen = true; + } + }); + }} + /> + +
Date: Thu, 21 Nov 2024 16:33:41 -0800 Subject: [PATCH 036/108] Filter parameter possibilities by resulting types if any. List all resulting type parameter variants as possibilities in dynamic filter. --- .../ActivityFilterBuilder.svelte | 22 ++++-- .../form/TimelineEditor/DynamicFilter.svelte | 78 +++++++++++-------- 2 files changed, 59 insertions(+), 41 deletions(-) diff --git a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte index 74d89a13b2..ad2cfa232d 100644 --- a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte +++ b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte @@ -11,6 +11,7 @@ import { activityTypes, subsystemTags } from '../../../../stores/plan'; import { spans, spanUtilityMaps } from '../../../../stores/simulation'; import { tags } from '../../../../stores/tags'; + import type { ValueSchemaVariant } from '../../../../types/schema'; import type { ActivityLayerDynamicFilter, ActivityLayerFilter } from '../../../../types/timeline'; import { compare } from '../../../../utilities/generic'; import { @@ -166,8 +167,7 @@ return lowercase(type.name).indexOf(lowercase(resultingTypesInputValue)) > -1; }); - // TODO need to get the list of matching types and then grab the actual applied filter? - $: allParameterTypes = $activityTypes.reduce((acc, activityType) => { + $: allParameterTypes = (matchingTypes.length ? matchingTypes : $activityTypes).reduce((acc, activityType) => { Object.entries(activityType.parameters).forEach(([parameterName, parameter]) => { const parameterType = parameter.schema.type; // TODO support series and struct? @@ -175,10 +175,18 @@ return; } const key = `${parameterName} (${parameterType})`; - const matchingName = !!acc[parameterType]; - const matchingType = matchingName && acc[parameterType].parameter.type === parameterType; - if (!matchingName || !matchingType) { - const values = parameterType === 'variant' ? parameter.schema.variants.map(variant => variant.key) : null; + const matchingName = !!acc[key]; + const matchingEntry = matchingName && acc[key].type === parameterType; + const isVariant = parameterType === 'variant'; + let values = null; + // If we have a matching variant, add unique variants to the list + if (matchingEntry && isVariant) { + const variantValues = (parameter.schema as ValueSchemaVariant).variants.map(variant => variant.key); + values = Array.from(new Set([...variantValues, ...acc[key].values])); + acc[key].values = values; + } + if (!matchingEntry) { + const values = isVariant ? parameter.schema.variants.map(variant => variant.key) : null; acc[key] = { name: parameterName, type: parameterType, @@ -190,8 +198,8 @@ return acc; }, {}); + // TODO support key/value for values array? $: parameterSubfields = Object.values(allParameterTypes).sort((a, b) => compare(a.label, b.label)); - // TODO support key/value for values array $: filteredActivityTypes = $activityTypes.filter(type => { if (!manualInputValue) { diff --git a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte index 9a8de4f66f..f87e14718b 100644 --- a/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte +++ b/src/components/timeline/form/TimelineEditor/DynamicFilter.svelte @@ -26,7 +26,6 @@ | Record> | { Parameter: { subfields: SubfieldSchema[] } } > = {}; - export let verb: string = 'Where'; const dispatch = createEventDispatcher<{ @@ -37,7 +36,7 @@ let dirtyFilter = structuredClone(filter); let currentField = dirtyFilter.field as keyof typeof ActivityLayerFilterFieldType; let currentOperator: keyof typeof FilterOperator | null = dirtyFilter.operator; - let subfields: SubfieldSchema[] | null = schema.Parameter?.subfields || null; + let subfields: SubfieldSchema[] | null = null; let currentSubfieldLabel = dirtyFilter.field === 'Parameter' ? `${dirtyFilter.subfield?.name} (${dirtyFilter.subfield?.type})` : ''; let currentType: DynamicFilterDataType = 'string'; @@ -45,12 +44,12 @@ let operatorKeys: (keyof typeof FilterOperator)[] = []; let currentValuePossibilities: Array = []; + $: subfields = schema.Parameter?.subfields; + $: if (currentField !== 'Parameter') { currentSubfieldLabel = ''; operatorKeys = Object.keys(schema[currentField]) as (keyof typeof FilterOperator)[]; currentType = (schema[currentField][currentOperator] || Object.values(schema[currentField])[0]).type; - // TODO filter to only the types included - // TODO value possibilities should be the union of all of the variants in case foo.A and bar.A have diff variants of the same type currentValuePossibilities = schema[currentField][currentOperator] ? schema[currentField][currentOperator].values : Object.values(schema[currentField])[0].values; @@ -89,7 +88,11 @@ newFilter.subfield = { name: matchingSubfield.name, type: matchingSubfield.type }; } } - dispatch('change', { filter: newFilter }); + // Make sure the filter is different before dispatching a change + // since svelte reactivity will run this statement when subfields changes + if (JSON.stringify(newFilter) !== JSON.stringify(filter)) { + dispatch('change', { filter: newFilter }); + } } async function onTagsInputChange(event: TagsChangeEvent) { @@ -134,35 +137,37 @@ {/each} - {#if currentType === 'string'} - - {:else if currentType === 'int' || currentType === 'real'} - - {:else if currentType === 'boolean'} - - {:else if currentType === 'variant'} - - {:else if currentType === 'tag'} - {@const currentValueTags = (Array.isArray(currentValue) ? currentValue : []).map(t => - currentValuePossibilities.find(v => v.id === t), - )} - -
- -
- {/if} +
+ {#if currentType === 'string'} + + {:else if currentType === 'int' || currentType === 'real'} + + {:else if currentType === 'boolean'} + + {:else if currentType === 'variant'} + + {:else if currentType === 'tag'} + {@const currentValueTags = (Array.isArray(currentValue) ? currentValue : []).map(t => + currentValuePossibilities.find(v => v.id === t), + )} + +
+ +
+ {/if} +
@@ -161,7 +166,6 @@ {#each displayedOptions as displayedOption} - {displayedOption.display} + {/each}
@@ -192,23 +203,25 @@ } .selected-display { + align-items: center; color: inherit; column-gap: 6px; display: grid; grid-template-columns: auto 16px; + padding: 0px 4px; position: relative; } - .st-input { + .dropdown-search :global(.st-input) { background-color: var(--aerie-dropdown-background-color, var(--st-white)); } - .st-input.disabled { + .st-select.disabled { cursor: not-allowed; opacity: 0.5; } - .st-input.error { + .st-select.error { background-color: var(--st-input-error-background-color); } @@ -224,7 +237,7 @@ min-width: inherit; } - .settings-icon { + .icon-right { align-items: center; cursor: pointer; display: flex; @@ -249,4 +262,15 @@ .dropdown-items { overflow-y: auto; } + + .dropdown-item { + display: flex; + flex-direction: row; + gap: 4px; + } + + .dropdown-item-icon { + display: flex; + width: 24px; + } From 5f4930f8e3ded9073a9e7d8a10d3881d9e8559f4 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Fri, 22 Nov 2024 08:03:17 -0800 Subject: [PATCH 038/108] Popper positioning workaround when inside css transformed parent. Use SearchableDropdown for parameters. --- .../form/TimelineEditor/Draggable.svelte | 14 +++++++++ .../form/TimelineEditor/DynamicFilter.svelte | 31 +++++++++++++++---- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/components/timeline/form/TimelineEditor/Draggable.svelte b/src/components/timeline/form/TimelineEditor/Draggable.svelte index 63f7713657..19edc8e1d4 100644 --- a/src/components/timeline/form/TimelineEditor/Draggable.svelte +++ b/src/components/timeline/form/TimelineEditor/Draggable.svelte @@ -10,6 +10,19 @@ export let initialHeight: number = 500; let rootRef: HTMLDivElement; + + const marginTransform = ({ + offsetX, + offsetY, + rootNode, + }: { + offsetX: number; + offsetY: number; + rootNode: HTMLElement; + }) => { + rootNode.style.marginLeft = `${offsetX}px`; + rootNode.style.marginTop = `${offsetY}px`; + };
@@ -126,11 +134,18 @@ {/each} {#if currentField === 'Parameter' && subfields} - +
+ ({ display: subfield.label, value: subfield.label }))} + > + + +
{/if} {:else if currentType === 'variant'} @@ -190,6 +205,10 @@ width: 40px; } + .dynamic-filter-searchable-dropdown { + overflow: hidden; + } + .dynamic-filter-value { flex: 1; min-width: 40px; From aa2cdc736b618564ef88c0577ffd7c23c151532a Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 25 Nov 2024 13:25:55 -0800 Subject: [PATCH 039/108] Fixes and refactoring --- src/components/ActivityList.svelte | 4 +- src/components/TimelineItemList.svelte | 24 ++++--- src/components/timeline/DropTarget.svelte | 4 +- src/components/timeline/Row.svelte | 38 ++++++++--- .../ActivityFilterBuilder.svelte | 16 ++--- src/stores/views.ts | 63 +++++++++++++++++-- src/types/timeline.ts | 9 ++- src/utilities/timeline.ts | 1 + 8 files changed, 123 insertions(+), 36 deletions(-) diff --git a/src/components/ActivityList.svelte b/src/components/ActivityList.svelte index a954b11dc4..946f002ade 100644 --- a/src/components/ActivityList.svelte +++ b/src/components/ActivityList.svelte @@ -7,7 +7,7 @@ import TimelineItemList from './TimelineItemList.svelte'; function getFilterValueFromItem(item: TimelineItemType) { - return (item as ActivityType).subsystem_tag?.id.toString() ?? ''; + return (item as ActivityType).subsystem_tag?.id ?? -1; } @@ -17,6 +17,6 @@ typeName="activity" typeNamePlural="Activities" {getFilterValueFromItem} - filterOptions={$subsystemTags.map(s => ({ color: s.color || '', label: s.name, value: s.id.toString() }))} + filterOptions={$subsystemTags.map(s => ({ color: s.color || '', label: s.name, value: s.id }))} filterName="Subsystem" /> diff --git a/src/components/TimelineItemList.svelte b/src/components/TimelineItemList.svelte index 0e3314d6a3..40e366afdc 100644 --- a/src/components/TimelineItemList.svelte +++ b/src/components/TimelineItemList.svelte @@ -28,7 +28,7 @@ export let items: TimelineItemType[] = []; export let filterOptions: TimelineItemListFilterOption[] = []; export let filterName: string = 'Filter'; - export let getFilterValueFromItem: (item: TimelineItemType) => string; + export let getFilterValueFromItem: (item: TimelineItemType) => string | number; let menu: Menu; let filteredItems: TimelineItemType[] = []; @@ -92,8 +92,9 @@ dragImage.className = 'st-typography-medium'; document.body.appendChild(dragImage); if (event.dataTransfer) { + const metadata = { selectedFilters, textFilters }; event.dataTransfer.setDragImage(dragImage, 0, 0); - event.dataTransfer.setData('text/plain', JSON.stringify({ items, type: typeName })); + event.dataTransfer.setData('text/plain', JSON.stringify({ items, metadata, type: typeName })); event.dataTransfer.dropEffect = typeName === 'activity' ? 'copy' : 'link'; event.dataTransfer.effectAllowed = typeName === 'activity' ? 'copyLink' : 'link'; } @@ -115,7 +116,8 @@ function onBulkLayerPicked(event: CustomEvent<{ layer?: Layer; row?: Row }>) { addTextFilter(); - viewAddFilterToRow(filteredItems, typeName, event.detail.row?.id, event.detail.layer); + const metadata = { selectedFilters, textFilters }; + viewAddFilterToRow(filteredItems, typeName, metadata, event.detail.row?.id, event.detail.layer); } function onBulkAddToRow(e: MouseEvent) { @@ -126,7 +128,7 @@ function onIndividualLayerPicked(event: CustomEvent<{ item?: TimelineItemType; layer?: Layer; row?: Row }>) { if (event.detail.item) { - viewAddFilterToRow([event.detail.item], typeName, event.detail.row?.id, event.detail.layer); + viewAddFilterToRow([event.detail.item], typeName, {}, event.detail.row?.id, event.detail.layer); } } @@ -162,16 +164,16 @@ {/if} {#each filterOptions as option} -