Skip to content

Commit

Permalink
support drilling down into executions by target and mnemonic (#8392)
Browse files Browse the repository at this point in the history
tacking this onto some existing code for string dimensions--would be
nice to be on a different filters ui, but that's way more work.

to reduce clutter a bit + since these filters don't apply outside of
drilldowns, i've made it so that the text inputs in the filters menu
only show up if the user is already filtering (i.e., they've clicked a
drilldown). we can add some links elsewhere in the UI to see this page
with the filter pre-set like we did for executions per-worker on the
executors page.
  • Loading branch information
jdhollen authored Feb 12, 2025
1 parent e84dd19 commit 720aac9
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 14 deletions.
54 changes: 54 additions & 0 deletions enterprise/app/filter/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
SortDesc,
Cloud,
Sparkles,
Target,
CircleDot,
} from "lucide-react";
import Checkbox from "../../../app/components/checkbox/checkbox";
import Radio from "../../../app/components/radio/radio";
Expand Down Expand Up @@ -62,6 +64,7 @@ import {
DURATION_SLIDER_MAX_VALUE,
getFiltersFromDimensionParam,
getDimensionName,
getDimensionParamFromFilters,
} from "./filter_util";
import TextInput from "../../../app/components/input/input";
import DatePickerButton from "./date_picker_button";
Expand All @@ -88,6 +91,7 @@ interface State {
minimumDuration?: number;
maximumDuration?: number;

dimensions?: string;
genericFilterString?: string;

sortBy?: SortBy;
Expand Down Expand Up @@ -116,6 +120,7 @@ export default class FilterComponent extends React.Component<FilterProps, State>
(capabilities.config.tagsUiEnabled && search.get(TAG_PARAM_NAME)) ||
search.get(MINIMUM_DURATION_PARAM_NAME) ||
search.get(MAXIMUM_DURATION_PARAM_NAME) ||
search.get(DIMENSION_PARAM_NAME) ||
search.get(GENERIC_FILTER_PARAM_NAME)
);
}
Expand All @@ -138,6 +143,7 @@ export default class FilterComponent extends React.Component<FilterProps, State>
maximumDuration: Number(search.get(MAXIMUM_DURATION_PARAM_NAME)) || undefined,
sortBy: (search.get(SORT_BY_PARAM_NAME) as SortBy) || undefined,
sortOrder: (search.get(SORT_ORDER_PARAM_NAME) as SortOrder) || undefined,
dimensions: search.get(DIMENSION_PARAM_NAME) || undefined,
genericFilterString: search.get(GENERIC_FILTER_PARAM_NAME) || undefined,
};
}
Expand All @@ -157,6 +163,7 @@ export default class FilterComponent extends React.Component<FilterProps, State>
maximumDuration: Number(search.get(MAXIMUM_DURATION_PARAM_NAME)) || undefined,
sortBy: (search.get(SORT_BY_PARAM_NAME) as SortBy) || undefined,
sortOrder: (search.get(SORT_ORDER_PARAM_NAME) as SortOrder) || undefined,
dimensions: search.get(DIMENSION_PARAM_NAME) || undefined,
genericFilterString: search.get(GENERIC_FILTER_PARAM_NAME) || undefined,
};
}
Expand Down Expand Up @@ -270,6 +277,7 @@ export default class FilterComponent extends React.Component<FilterProps, State>
[COMMAND_PARAM_NAME]: this.state.command || "",
[PATTERN_PARAM_NAME]: this.state.pattern || "",
[TAG_PARAM_NAME]: this.state.tag || "",
[DIMENSION_PARAM_NAME]: this.state.dimensions || "",
[MINIMUM_DURATION_PARAM_NAME]: this.state.minimumDuration?.toString() || "",
[MAXIMUM_DURATION_PARAM_NAME]: this.state.maximumDuration?.toString() || "",
[GENERIC_FILTER_PARAM_NAME]: this.state.genericFilterString || "",
Expand All @@ -294,6 +302,47 @@ export default class FilterComponent extends React.Component<FilterProps, State>
);
}

updateDimensionFilters(dimension: stat_filter.Dimension, value: string) {
const filters = getFiltersFromDimensionParam(this.props.search.get(DIMENSION_PARAM_NAME) ?? "")
.map((f) => {
if (f.dimension?.execution === dimension.execution && f.dimension?.invocation === dimension.invocation) {
if (!value) {
return undefined;
}
return new stat_filter.DimensionFilter({ dimension, value });
}
return f;
})
.filter((f) => f !== undefined);
this.setState({ dimensions: getDimensionParamFromFilters(filters) });
}

maybeRenderDimensionFilterInputs() {
const existingFilters = getFiltersFromDimensionParam(this.props.search.get(DIMENSION_PARAM_NAME) ?? "");
const pendingFilters = getFiltersFromDimensionParam(this.state.dimensions ?? "");
return existingFilters.map((f) => {
const dimension = f.dimension;
if (!dimension) {
return <></>;
}
const pendingFilter = pendingFilters.find(
(f) => f.dimension?.execution === dimension.execution && f.dimension?.invocation === dimension.invocation
);
return (
<>
<div className="option-group-title">{getDimensionName(dimension)}</div>
<div className="option-group-input">
<TextInput
placeholder={""}
value={pendingFilter?.value ?? ""}
onChange={(e) => this.updateDimensionFilters(dimension, e.target.value)}
/>
</div>
</>
);
});
}

render() {
const roleValue = this.props.search.get(ROLE_PARAM_NAME) || "";
const statusValue = this.props.search.get(STATUS_PARAM_NAME) || "";
Expand Down Expand Up @@ -517,6 +566,7 @@ export default class FilterComponent extends React.Component<FilterProps, State>
</div>
</>
)}
{this.maybeRenderDimensionFilterInputs()}
<div className="option-group-title">Duration</div>
<div className="option-group-input">
<Slider
Expand Down Expand Up @@ -621,6 +671,10 @@ function getDimensionIcon(f: stat_filter.Dimension) {
switch (f.execution) {
case stat_filter.ExecutionDimensionType.WORKER_EXECUTION_DIMENSION:
return <Cloud />;
case stat_filter.ExecutionDimensionType.TARGET_LABEL_EXECUTION_DIMENSION:
return <Target />;
case stat_filter.ExecutionDimensionType.ACTION_MNEMONIC_EXECUTION_DIMENSION:
return <CircleDot />;
}
} else if (f.invocation) {
switch (f.invocation) {
Expand Down
25 changes: 24 additions & 1 deletion enterprise/app/filter/filter_util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,25 @@ function parseDimensionType(stringValue: string): stat_filter.Dimension | undefi
return undefined;
}

export function getDimensionParamFromFilters(filters: stat_filter.DimensionFilter[]): string {
const filterStrings = filters
.map((f) => {
if (!(f.dimension?.execution || f.dimension?.invocation) || !f.value) {
return undefined;
}
let dimensionName = "";
if (f.dimension.execution) {
dimensionName = "e" + f.dimension.execution.toString();
} else {
dimensionName = "i" + f.dimension.invocation!.toString();
}
return dimensionName + "|" + f.value.length + "|" + f.value;
})
.filter((f) => f !== undefined);

return filterStrings.join("|");
}

// Parses a set of DimensionFilters from the supplied URL param string. Each
// entry is formatted as "dimension|value_length|value" and entry is separated
// from the next by another pipe.
Expand Down Expand Up @@ -141,7 +160,7 @@ export function getFiltersFromDimensionParam(dimensionParamValue: string): stat_
const value = dimensionParamValue.substring(0, dimensionValueLength);
filters.push(new stat_filter.DimensionFilter({ dimension, value }));

dimensionParamValue = dimensionParamValue.substring(dimensionValueLength);
dimensionParamValue = dimensionParamValue.substring(dimensionValueLength + 1);
}
return filters;
}
Expand Down Expand Up @@ -385,6 +404,10 @@ export function getDimensionName(d: stat_filter.Dimension): string {
switch (d.execution) {
case stat_filter.ExecutionDimensionType.WORKER_EXECUTION_DIMENSION:
return "Worker";
case stat_filter.ExecutionDimensionType.TARGET_LABEL_EXECUTION_DIMENSION:
return "Target";
case stat_filter.ExecutionDimensionType.ACTION_MNEMONIC_EXECUTION_DIMENSION:
return "Mnemonic";
}
} else if (d.invocation) {
switch (d.invocation) {
Expand Down
8 changes: 8 additions & 0 deletions enterprise/app/trends/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,11 @@ export function encodeMetricUrlParam(metric: stat_filter.Metric): string {
export function encodeWorkerUrlParam(workerId: string): string {
return `e1|${workerId.length}|${workerId}`;
}

export function encodeTargetLabelUrlParam(targetLabel: string): string {
return `e2|${targetLabel.length}|${targetLabel}`;
}

export function encodeActionMnemonicUrlParam(actionMnemonic: string): string {
return `e3|${actionMnemonic.length}|${actionMnemonic}`;
}
31 changes: 29 additions & 2 deletions enterprise/app/trends/drilldown_page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import { User } from "../../../app/auth/user";
import Select, { Option } from "../../../app/components/select/select";
import router from "../../../app/router/router";
import { CategoricalChartState } from "recharts/types/chart/types";
import { encodeMetricUrlParam, encodeWorkerUrlParam } from "./common";
import {
encodeActionMnemonicUrlParam,
encodeMetricUrlParam,
encodeTargetLabelUrlParam,
encodeWorkerUrlParam,
} from "./common";

const DD_SELECTED_METRIC_URL_PARAM: string = "ddMetric";
const DD_SELECTED_AREA_URL_PARAM = "ddSelection";
Expand Down Expand Up @@ -610,14 +615,30 @@ export default class DrilldownPageComponent extends React.Component<Props, State
}
return;
case stats.DrilldownType.WORKER_DRILLDOWN_TYPE:
this.navigateForBarClick("d", encodeWorkerUrlParam(e.activeLabel));
this.navigateDimensionBarClick(encodeWorkerUrlParam(e.activeLabel));
return;
case stats.DrilldownType.TARGET_LABEL_DRILLDOWN_TYPE:
this.navigateDimensionBarClick(encodeTargetLabelUrlParam(e.activeLabel));
return;
case stats.DrilldownType.ACTION_MNEMONIC_DRILLDOWN_TYPE:
this.navigateDimensionBarClick(encodeActionMnemonicUrlParam(e.activeLabel));
return;
case stats.DrilldownType.GROUP_ID_DRILLDOWN_TYPE:
case stats.DrilldownType.DATE_DRILLDOWN_TYPE:
default:
return;
}
}

navigateDimensionBarClick(newParam: string) {
let result = this.props.search.get("d") ?? "";
if (result) {
result += "|";
}
result += newParam;
this.navigateForBarClick("d", result);
}

formatDrilldownType(d: stats.DrilldownType) {
switch (d) {
case stats.DrilldownType.USER_DRILLDOWN_TYPE:
Expand All @@ -638,6 +659,10 @@ export default class DrilldownPageComponent extends React.Component<Props, State
return "tag";
case stats.DrilldownType.WORKER_DRILLDOWN_TYPE:
return "worker (execution)";
case stats.DrilldownType.TARGET_LABEL_DRILLDOWN_TYPE:
return "target (execution)";
case stats.DrilldownType.ACTION_MNEMONIC_DRILLDOWN_TYPE:
return "mnemonic (execution)";
default:
return "???";
}
Expand Down Expand Up @@ -856,6 +881,8 @@ export default class DrilldownPageComponent extends React.Component<Props, State
dataKey={(entry: stats.DrilldownEntry) => entry.label}
/>
<Tooltip
allowEscapeViewBox={{ x: true, y: true }}
wrapperStyle={{ zIndex: 1 }}
content={this.renderCustomTooltip.bind(
this,
this.formatDrilldownType(chart.drilldownType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ func (i *InvocationStatService) getDrilldownQuery(ctx context.Context, req *stpb
drilldownFields = append(drilldownFields, "tag")
}
if req.GetDrilldownMetric().Execution != nil {
drilldownFields = append(drilldownFields, "worker")
drilldownFields = append(drilldownFields, "worker", "target_label", "action_mnemonic")
}
placeholderQuery := query_builder.NewQuery("")

Expand Down Expand Up @@ -1283,16 +1283,18 @@ func (i *InvocationStatService) GetStatDrilldown(ctx context.Context, req *stpb.
m := make(map[stpb.DrilldownType]*stpb.DrilldownChart)
dm := make(map[stpb.DrilldownType]float64)
type queryOut struct {
GormUser *string
GormHost *string
GormRepoURL *string
GormBranchName *string
GormCommitSHA *string
GormPattern *string
GormWorker *string
GormTag *string
Selection int64
Inverse int64
GormUser *string
GormHost *string
GormRepoURL *string
GormBranchName *string
GormCommitSHA *string
GormPattern *string
GormWorker *string
GormTag *string
GormTargetLabel *string
GormActionMnemonic *string
Selection int64
Inverse int64
}

firstRow := true
Expand Down Expand Up @@ -1321,6 +1323,10 @@ func (i *InvocationStatService) GetStatDrilldown(ctx context.Context, req *stpb.
addOutputChartEntry(m, dm, stpb.DrilldownType_PATTERN_DRILLDOWN_TYPE, stat.GormPattern, stat.Inverse, stat.Selection, rsp.TotalInBase, rsp.TotalInSelection)
} else if stat.GormWorker != nil {
addOutputChartEntry(m, dm, stpb.DrilldownType_WORKER_DRILLDOWN_TYPE, stat.GormWorker, stat.Inverse, stat.Selection, rsp.TotalInBase, rsp.TotalInSelection)
} else if stat.GormTargetLabel != nil {
addOutputChartEntry(m, dm, stpb.DrilldownType_TARGET_LABEL_DRILLDOWN_TYPE, stat.GormTargetLabel, stat.Inverse, stat.Selection, rsp.TotalInBase, rsp.TotalInSelection)
} else if stat.GormActionMnemonic != nil {
addOutputChartEntry(m, dm, stpb.DrilldownType_ACTION_MNEMONIC_DRILLDOWN_TYPE, stat.GormActionMnemonic, stat.Inverse, stat.Selection, rsp.TotalInBase, rsp.TotalInSelection)
} else if stat.GormTag != nil {
addOutputChartEntry(m, dm, stpb.DrilldownType_TAG_DRILLDOWN_TYPE, stat.GormTag, stat.Inverse, stat.Selection, rsp.TotalInBase, rsp.TotalInSelection)
} else {
Expand Down
2 changes: 2 additions & 0 deletions proto/stat_filter.proto
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ enum InvocationDimensionType {
enum ExecutionDimensionType {
UNKNOWN_EXECUTION_DIMENSION = 0;
WORKER_EXECUTION_DIMENSION = 1;
TARGET_LABEL_EXECUTION_DIMENSION = 2;
ACTION_MNEMONIC_EXECUTION_DIMENSION = 3;
}

message Dimension {
Expand Down
2 changes: 2 additions & 0 deletions proto/stats.proto
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ enum DrilldownType {
PATTERN_DRILLDOWN_TYPE = 8;
WORKER_DRILLDOWN_TYPE = 9;
TAG_DRILLDOWN_TYPE = 10;
TARGET_LABEL_DRILLDOWN_TYPE = 11;
ACTION_MNEMONIC_DRILLDOWN_TYPE = 12;
}

message DrilldownChart {
Expand Down
4 changes: 4 additions & 0 deletions server/util/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ func executionDimensionToDbField(m stat_filter.ExecutionDimensionType) (string,
switch m {
case stat_filter.ExecutionDimensionType_WORKER_EXECUTION_DIMENSION:
return "worker", nil
case stat_filter.ExecutionDimensionType_TARGET_LABEL_EXECUTION_DIMENSION:
return "target_label", nil
case stat_filter.ExecutionDimensionType_ACTION_MNEMONIC_EXECUTION_DIMENSION:
return "action_mnemonic", nil
default:
return "", status.InvalidArgumentErrorf("Invalid field: %s", m.String())
}
Expand Down

0 comments on commit 720aac9

Please sign in to comment.