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

Changes reorder column events to use source and destination #6733

Merged
merged 14 commits into from
Jan 31, 2024
Merged
2 changes: 2 additions & 0 deletions tensorboard/webapp/hparams/_redux/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ tf_ts_library(
"//tensorboard/webapp/hparams:_types",
"//tensorboard/webapp/runs/actions",
"//tensorboard/webapp/widgets/data_table:types",
"//tensorboard/webapp/widgets/data_table:utils",
"@npm//@ngrx/store",
],
)
Expand Down Expand Up @@ -174,6 +175,7 @@ tf_ts_library(
"//tensorboard/webapp/util:types",
"//tensorboard/webapp/webapp_data_source:http_client_testing",
"//tensorboard/webapp/widgets/data_table:types",
"//tensorboard/webapp/widgets/data_table:utils",
"@npm//@ngrx/effects",
"@npm//@ngrx/store",
"@npm//@types/jasmine",
Expand Down
29 changes: 7 additions & 22 deletions tensorboard/webapp/hparams/_redux/hparams_reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
import {Action, ActionReducer, createReducer, on} from '@ngrx/store';
import {ColumnHeader, Side} from '../../widgets/data_table/types';
import {Side} from '../../widgets/data_table/types';
import {DataTableUtils} from '../../widgets/data_table/utils';
import * as actions from './hparams_actions';
import {HparamsState} from './types';

Expand Down Expand Up @@ -141,28 +142,12 @@ const reducer: ActionReducer<HparamsState, Action> = createReducer(
actions.dashboardHparamColumnOrderChanged,
(state, {source, destination, side}) => {
const {dashboardDisplayedHparamColumns: columns} = state;
const sourceIndex = columns.findIndex(
(column: ColumnHeader) => column.name === source.name
const newColumns = DataTableUtils.moveColumn(
columns,
source,
destination,
side
);
let destinationIndex = columns.findIndex(
(column: ColumnHeader) => column.name === destination.name
);
if (sourceIndex === -1 || sourceIndex === destinationIndex) {
return state;
}
if (destinationIndex === -1) {
// Use side as a backup to determine source position if destination isn't found.
if (side !== undefined) {
destinationIndex = side === Side.LEFT ? 0 : columns.length - 1;
} else {
return state;
}
}

const newColumns = [...columns];
newColumns.splice(sourceIndex, 1);
newColumns.splice(destinationIndex, 0, source);

return {
...state,
dashboardDisplayedHparamColumns: newColumns,
Expand Down
107 changes: 13 additions & 94 deletions tensorboard/webapp/hparams/_redux/hparams_reducers_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as actions from './hparams_actions';
import {reducers} from './hparams_reducers';
import {buildHparamSpec, buildHparamsState, buildMetricSpec} from './testing';
import {ColumnHeaderType, Side} from '../../widgets/data_table/types';
import {DataTableUtils} from '../../widgets/data_table/utils';

describe('hparams/_redux/hparams_reducers_test', () => {
describe('hparamsFetchSessionGroupsSucceeded', () => {
Expand Down Expand Up @@ -594,104 +595,15 @@ describe('hparams/_redux/hparams_reducers_test', () => {
},
];

it('does nothing if source is not found', () => {
it('moves source to destination using moveColumn', () => {
const state = buildHparamsState({
dashboardDisplayedHparamColumns: fakeColumns,
});
const state2 = reducers(
state,
actions.dashboardHparamColumnOrderChanged({
source: {
type: ColumnHeaderType.HPARAM,
name: 'nonexistent_param',
displayName: 'Nonexistent param',
enabled: false,
},
destination: {
type: ColumnHeaderType.HPARAM,
name: 'conv_kernel_size',
displayName: 'Conv Kernel Size',
enabled: true,
},
side: Side.LEFT,
})
);

expect(state2.dashboardDisplayedHparamColumns).toEqual(fakeColumns);
});

it('does nothing if source equals dest', () => {
const state = buildHparamsState({
dashboardDisplayedHparamColumns: fakeColumns,
});
const state2 = reducers(
state,
actions.dashboardHparamColumnOrderChanged({
source: {
type: ColumnHeaderType.HPARAM,
name: 'conv_kernel_size',
displayName: 'Conv Kernel Size',
enabled: false,
},
destination: {
type: ColumnHeaderType.HPARAM,
name: 'conv_kernel_size',
displayName: 'Conv Kernel Size',
enabled: true,
},
side: Side.LEFT,
})
);

expect(state2.dashboardDisplayedHparamColumns).toEqual(fakeColumns);
});
const moveColumnSpy = spyOn(
DataTableUtils,
'moveColumn'
).and.callThrough();

[
{
testDesc: 'to front if side is left',
side: Side.LEFT,
expectedResult: [
fakeColumns[1],
fakeColumns[0],
...fakeColumns.slice(2),
],
},
{
testDesc: 'to back if side is right',
side: Side.RIGHT,
expectedResult: [
fakeColumns[0],
...fakeColumns.slice(2),
fakeColumns[1],
],
},
].forEach(({testDesc, side, expectedResult}) => {
it(`if destination not found, moves source ${testDesc}`, () => {
const state = buildHparamsState({
dashboardDisplayedHparamColumns: fakeColumns,
});
const state2 = reducers(
state,
actions.dashboardHparamColumnOrderChanged({
source: fakeColumns[1],
destination: {
type: ColumnHeaderType.HPARAM,
name: 'nonexistent param',
displayName: 'Nonexistent param',
enabled: true,
},
side,
})
);

expect(state2.dashboardDisplayedHparamColumns).toEqual(expectedResult);
});
});

it('swaps source and destination positions if destination is found', () => {
const state = buildHparamsState({
dashboardDisplayedHparamColumns: fakeColumns,
});
const state2 = reducers(
state,
actions.dashboardHparamColumnOrderChanged({
Expand All @@ -701,6 +613,13 @@ describe('hparams/_redux/hparams_reducers_test', () => {
})
);

// Edge cases are tested by moveColumn tests.
rileyajones marked this conversation as resolved.
Show resolved Hide resolved
expect(moveColumnSpy).toHaveBeenCalledWith(
fakeColumns,
fakeColumns[1],
fakeColumns[0],
Side.LEFT
);
expect(state2.dashboardDisplayedHparamColumns).toEqual([
fakeColumns[1],
fakeColumns[0],
Expand Down
4 changes: 2 additions & 2 deletions tensorboard/webapp/metrics/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ export const sortingDataTable = createAction(
props<SortingInfo>()
);

export const dataTableColumnEdited = createAction(
'[Metrics] Data table columns edited in edit menu',
export const dataTableColumnOrderChanged = createAction(
'[Metrics] Data table columns order changed',
props<HeaderEditInfo>()
);

Expand Down
7 changes: 3 additions & 4 deletions tensorboard/webapp/metrics/internal_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {TimeSelection} from '../widgets/card_fob/card_fob_types';
import {HistogramMode} from '../widgets/histogram/histogram_types';
import {
ColumnHeader,
ColumnHeaderType,
DataTableMode,
ReorderColumnEvent,
} from '../widgets/data_table/types';

export {HistogramMode, TimeSelection};
Expand Down Expand Up @@ -94,15 +94,14 @@ export interface URLDeserializedState {
};
}

export interface HeaderEditInfo {
export interface HeaderEditInfo extends ReorderColumnEvent {
dataTableMode: DataTableMode;
headers: ColumnHeader[];
}

export interface HeaderToggleInfo {
header: ColumnHeader;
cardId?: CardId;
dataTableMode?: DataTableMode;
dataTableMode?: DataTableMode | undefined;
}

export const SCALARS_SMOOTHING_MIN = 0;
Expand Down
102 changes: 26 additions & 76 deletions tensorboard/webapp/metrics/store/metrics_reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ import {
URLDeserializedState,
} from '../types';
import {groupCardIdWithMetdata} from '../utils';
import {
ColumnHeader,
ColumnHeaderType,
DataTableMode,
} from '../../widgets/data_table/types';
import {ColumnHeaderType, DataTableMode} from '../../widgets/data_table/types';
import {
buildOrReturnStateWithPinnedCopy,
buildOrReturnStateWithUnresolvedImportedPins,
Expand Down Expand Up @@ -82,6 +78,7 @@ import {
TimeSeriesData,
TimeSeriesLoadable,
} from './metrics_types';
import {DataTableUtils} from '../../widgets/data_table/utils';

function buildCardMetadataList(tagMetadata: TagMetadata): CardMetadata[] {
const results: CardMetadata[] = [];
Expand Down Expand Up @@ -1434,34 +1431,30 @@ const reducer = createReducer(
tableEditorSelectedTab: tab,
};
}),
on(actions.dataTableColumnEdited, (state, {dataTableMode, headers}) => {
const enabledNewHeaders: ColumnHeader[] = [];
const disabledNewHeaders: ColumnHeader[] = [];

// All enabled headers appear above all disabled headers.
headers.forEach((header) => {
if (header.enabled) {
enabledNewHeaders.push(header);
} else {
disabledNewHeaders.push(header);
on(
actions.dataTableColumnOrderChanged,
(state, {source, destination, side, dataTableMode}) => {
let headers =
rileyajones marked this conversation as resolved.
Show resolved Hide resolved
dataTableMode === DataTableMode.RANGE
? [...state.rangeSelectionHeaders]
Copy link
Contributor

Choose a reason for hiding this comment

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

Previously the headers were retrieved from the action, now they are being retrieved from the current state. How can the headers ever change?

Copy link
Member Author

Choose a reason for hiding this comment

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

To clarify:

  • dataTableColumnEdited is currently only used for reordering
  • runs and hparams features already define separate actions for reordering and adding (e.g. runsTableHeaderAdded, runsTableHeaderOrderChanged)

To prevent confusion (and for consistency) I'll change dataTableColumnEdited -> dataTableColumnOrderChanged

Copy link
Member Author

Choose a reason for hiding this comment

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

(a separate dataTableColumnAdded action will be added in an upcoming PR)

Copy link
Contributor

Choose a reason for hiding this comment

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

In that case can you headers from the props?

Copy link
Member Author

@hoonji hoonji Jan 30, 2024

Choose a reason for hiding this comment

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

Wait sorry - I need to correct myself. I did add a dataTableColumnAdded in #6737, but it's unused because we don't actually have a feature for adding standard columns, only hparam columns. Thus, only the hparams action dashboardHparamColumnAdded will be used to add new columns.

Regarding passing headers to the props, I propose that an action signature like dashboardHparamColumnAdded, i.e. only specifying {column, nextTo, side} and letting the reducer figure out how put it in the right place, is more in line with redux best practices, which advocate putting logic in reducers, not components; passing in headers via the action implies that the component must do some data manipulation first.

(Note that the previously implemented runs table add action already uses the "put logic in reducers" pattern, albeit with a slightly different signature.)

Copy link
Contributor

Choose a reason for hiding this comment

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

I fully agree with everything in this statement.
The thing that I find confusing is that the action dataTableColumnEdited takes HeaderEditInfo as props which has an unused attribute headers

: [...state.singleSelectionHeaders];
headers = DataTableUtils.moveColumn(headers, source, destination, side);

if (dataTableMode === DataTableMode.RANGE) {
return {
...state,
rangeSelectionHeaders: headers,
};
}
});

if (dataTableMode === DataTableMode.RANGE) {
return {
...state,
rangeSelectionHeaders: enabledNewHeaders.concat(disabledNewHeaders),
singleSelectionHeaders: headers,
};
}

return {
...state,
singleSelectionHeaders: enabledNewHeaders.concat(disabledNewHeaders),
};
}),
),
on(
actions.dataTableColumnToggled,
(state, {dataTableMode, header, cardId}) => {
(state, {dataTableMode, header: toggledHeader, cardId}) => {
const {cardStateMap, rangeSelectionEnabled, linkedTimeEnabled} = state;
const rangeEnabled = cardId
? cardRangeSelectionEnabled(
Expand All @@ -1471,40 +1464,24 @@ const reducer = createReducer(
cardId
)
: dataTableMode === DataTableMode.RANGE;

const targetedHeaders = rangeEnabled
? state.rangeSelectionHeaders
: state.singleSelectionHeaders;

const currentToggledHeaderIndex = targetedHeaders.findIndex(
(element) => element.name === header.name
);

// If the header is being enabled it goes at the bottom of the currently
// enabled headers. If it is being disabled it goes to the top of the
// currently disabled headers.
let newToggledHeaderIndex = getEnabledCount(targetedHeaders);
if (targetedHeaders[currentToggledHeaderIndex].enabled) {
newToggledHeaderIndex--;
}
const newHeaders = moveHeader(
currentToggledHeaderIndex,
newToggledHeaderIndex,
targetedHeaders
);

newHeaders[newToggledHeaderIndex] = {
...newHeaders[newToggledHeaderIndex],
enabled: !newHeaders[newToggledHeaderIndex].enabled,
};
const newHeaders = targetedHeaders.map((header) => {
rileyajones marked this conversation as resolved.
Show resolved Hide resolved
const newHeader = {...header};
if (header.name === toggledHeader.name) {
newHeader.enabled = !newHeader.enabled;
}
return newHeader;
});

if (rangeEnabled) {
return {
...state,
rangeSelectionHeaders: newHeaders,
};
}

return {
...state,
singleSelectionHeaders: newHeaders,
Expand Down Expand Up @@ -1583,30 +1560,3 @@ function buildTagToRuns(runTagInfo: {[run: string]: string[]}) {
}
return tagToRuns;
}

/**
* Returns a copy of the headers array with item at sourceIndex moved to
* destinationIndex.
*/
function moveHeader(
sourceIndex: number,
destinationIndex: number,
headers: ColumnHeader[]
) {
const newHeaders = [...headers];
// Delete from original location
newHeaders.splice(sourceIndex, 1);
// Insert at destinationIndex.
newHeaders.splice(destinationIndex, 0, headers[sourceIndex]);
return newHeaders;
}

function getEnabledCount(headers: ColumnHeader[]) {
let count = 0;
headers.forEach((header) => {
if (header.enabled) {
count++;
}
});
return count;
}
Loading
Loading