Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dashboards): control legends individually #78675

Open
wants to merge 19 commits into
base: master
Choose a base branch
from

Conversation

nikkikapadia
Copy link
Member

@nikkikapadia nikkikapadia commented Oct 7, 2024

Previously charts that have the same series on their legend were controlled together (toggling one off would toggle them both off). Now the legend can be controlled individually as they are associated with the widget id. Since it is stored in the query (in the format widgetId-seriesName1-seriesName2-...), this also fixes the issue where selecting a new time range or other filter changes reset what series were selected.

video evidence:

Screen.Recording.2024-10-07.at.9.32.03.AM.mov

@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Oct 7, 2024
Copy link

codecov bot commented Oct 7, 2024

Codecov Report

Attention: Patch coverage is 63.21839% with 32 lines in your changes missing coverage. Please review.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...atic/app/views/dashboards/dashboardLegendUtils.tsx 67.64% 22 Missing ⚠️
...dashboards/widgetCard/widgetCardChartContainer.tsx 50.00% 3 Missing ⚠️
static/app/views/dashboards/detail.tsx 50.00% 2 Missing ⚠️
static/app/views/dashboards/widgetCard/chart.tsx 33.33% 2 Missing ⚠️
static/app/components/modals/widgetViewerModal.tsx 0.00% 1 Missing ⚠️
...rds/widgetBuilder/buildSteps/visualizationStep.tsx 50.00% 1 Missing ⚠️
...p/views/dashboards/widgetBuilder/widgetBuilder.tsx 66.66% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           master   #78675    +/-   ##
========================================
  Coverage   78.27%   78.27%            
========================================
  Files        7120     7127     +7     
  Lines      313496   313764   +268     
  Branches    51171    51221    +50     
========================================
+ Hits       245388   245612   +224     
- Misses      61679    61717    +38     
- Partials     6429     6435     +6     

@nikkikapadia nikkikapadia marked this pull request as ready for review October 7, 2024 15:18
@nikkikapadia nikkikapadia requested a review from a team as a code owner October 7, 2024 15:18
Copy link

codecov bot commented Oct 11, 2024

Bundle Report

Changes will decrease total bundle size by 93.58kB (-0.3%) ⬇️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
app-webpack-bundle-array-push 30.89MB 93.58kB (-0.3%) ⬇️

Copy link
Member

@gggritso gggritso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really great! I added a bunch of small feedback about naming and using fixtures, which I hope will be easy to search-and-replace. This is very very close!

Comment on lines 57 to 65
dashboard: {
id: 'new',
title: 'Dashboard',
createdBy: undefined,
dateCreated: '2020-01-01T00:00:00.000Z',
widgets: [widget],
projects: [],
filters: {},
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think dashboard: DashboardFixture() would be fine here. Applies to all the other spec files, too! If you can use the fixtures, it'd be a lot cleaner (I saw you did in one spot)

Comment on lines 1490 to 1503
const dashboardLegendUtils = new DashboardLegendEncoderDecoder({
location: initialData.router.location,
dashboard: {
id: 'new',
title: 'Dashboard',
createdBy: undefined,
dateCreated: '2020-01-01T00:00:00.000Z',
widgets: [mockWidget],
projects: [],
filters: {},
},
organization: initialData.organization,
router: initialData.router,
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you save this in a variable at the top of the spec and re-use for all tests?

Comment on lines 251 to 252
const [disabledLegends, setDisabledLegends] = useState<{[key: string]: boolean}>(
decodeList(location.query[WidgetViewerQueryField.LEGEND]).reduce((acc, legend) => {
acc[legend] = false;
return acc;
}, {})
dashboardLegendUtils.getLegendUnselected(widget)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since dashboardLegendUtils keeps all this stuff in the URL now, is it possible to remove disabledLegends and setDisabledLegends?

router: InjectedRouter;
};

class DashboardLegendEncoderDecoder {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accurate name, but I think WidgetLegendState or WidgetLegendSelectionState would be more evocative! ...State implies that the class is used to manage a state, which is true! Plus, removing "Dashboard" from the name is good, since we'll probably want to use this for non-Dashboard UIs in the future!

Oh also, the file name should match, so widgetLegendState.tsx or whichever you go with

}

// Updates legend param when a legend selection has been changed
updateLegendQueryParam(selected: Record<string, boolean>, widget: Widget) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think setWidgetSelectionState is clearer here. Maybe even

Suggested change
updateLegendQueryParam(selected: Record<string, boolean>, widget: Widget) {
setWidgetSelectionState(widget: Widget, selection: LegendSelection) {

And

type LegendSelection = Record<string, boolean>;

Somewhere higher up.

This way the class name nicely matches the kind of data that's passed around. WidgetLegendSelectionState manages widgets and selections

Comment on lines 141 to 143
const legendValues = legend.split('-');
const widgetId = legendValues[0];
const seriesNames = legendValues.splice(1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const legendValues = legend.split('-');
const widgetId = legendValues[0];
const seriesNames = legendValues.splice(1);
const [widgetId, ...seriesNames] = legend.split('-');

If you stick to using dash. Otherwise

const [widgetId, seriesNameString] = legend.split('-');
const seriesNames = seriesNameString.split(',');

If you go with commas

const legendValues = legend.split('-');
const widgetId = legendValues[0];
const seriesNames = legendValues.splice(1);
if (widget.id === widgetId && seriesNames) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine, but it would be nicer, I think, to use .find and find the right widget by ID right away after decoding.

Comment on lines 145 to 149
seriesNames.forEach(series => {
if (series) {
acc[this.encodeSeriesNameForLegend(series, widget.id)] = false;
}
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's possible for series to be empty, maybe series.map(Boolean).forEach would be a little cleaner

Comment on lines 155 to 173
encodeSeriesNameForLegend(seriesName: string, widgetId?: string) {
return `${seriesName}:${widgetId}`;
}

decodeSeriesNameForLegend(encodedSeriesName: string) {
return encodedSeriesName.split(':')[0];
}

// change timeseries names to SeriesName:widgetID
modifyTimeseriesNames(widget: Widget, timeseriesResults?: Series[]) {
return timeseriesResults
? timeseriesResults.map(series => {
return {
...series,
seriesName: this.encodeSeriesNameForLegend(series.seriesName, widget.id),
};
})
: [];
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this would be even better ins a separate class, or even as a set of helper functions!

}

// when a widget has been changed/added/deleted update legend to incorporate that
updatedLegendQueryOnWidgetChange(newDashboard: DashboardDetails) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 this one is a bit odd. Is it possible to call encodeLegendQueryParam for every new widget instead of this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Scope: Frontend Automatically applied to PRs that change frontend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants