Skip to content

Commit 411959d

Browse files
committed
Merge branch 'master' of github.com:scalableminds/webknossos into pricing
* 'master' of github.com:scalableminds/webknossos: Respect mail.smtp.auth setting (#6692) Implement multi selection of datasets in folder tab (#6683)
2 parents 34ff183 + 6469d1d commit 411959d

16 files changed

+475
-239
lines changed

CHANGELOG.unreleased.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1414
- Added sign in via OIDC. [#6534](https://github.com/scalableminds/webknossos/pull/6534)
1515
- Added a new datasets tab to the dashboard which supports managing datasets in folders. Folders can be organized hierarchically and datasets can be moved into these folders. Selecting a dataset will show dataset details in a sidebar. [#6591](https://github.com/scalableminds/webknossos/pull/6591)
1616
- Added the option to search a specific folder in the new datasets tab. [#6677](https://github.com/scalableminds/webknossos/pull/6677)
17+
- The new datasets tab in the dashboard allows multi-selection of datasets so that multiple datasets can be moved to a folder at once. As in typical file explorers, CTRL + left click adds individual datasets to the current selection. Shift + left click selects a range of datasets. [#6683](https://github.com/scalableminds/webknossos/pull/6683)
1718

1819
### Changed
1920
- The log viewer in the Voxelytics workflow reporting now uses a virtualized list. [#6579](https://github.com/scalableminds/webknossos/pull/6579)
@@ -36,6 +37,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
3637
- Fixed access of remote datasets using the Amazon S3 protocol [#6679](https://github.com/scalableminds/webknossos/pull/6679)
3738
- Fixed a bug in line measurement that would lead to an infinite loop. [#6689](https://github.com/scalableminds/webknossos/pull/6689)
3839
- Fixed a bug where malformed json files could lead to uncaught exceptions.[#6691](https://github.com/scalableminds/webknossos/pull/6691)
40+
- Respect the config value mail.smtp.auth (used to be ignored, always using true) [#6692](https://github.com/scalableminds/webknossos/pull/6692)
3941

4042
### Removed
4143

app/Startup.scala

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class Startup @Inject()(actorSystem: ActorSystem,
120120
conf.Mail.Smtp.host,
121121
conf.Mail.Smtp.port,
122122
conf.Mail.Smtp.tls,
123+
conf.Mail.Smtp.auth,
123124
conf.Mail.Smtp.user,
124125
conf.Mail.Smtp.pass,
125126
)

app/oxalis/mail/Mailer.scala

+6-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ case class MailerConfig(
2020
smtpHost: String,
2121
smtpPort: Int,
2222
smtpTls: Boolean,
23+
smtpAuth: Boolean,
2324
smtpUser: String,
2425
smtpPass: String,
2526
)
@@ -54,7 +55,9 @@ class Mailer(conf: MailerConfig) extends Actor with LazyLogging {
5455
multiPartMail.setHostName(conf.smtpHost)
5556
multiPartMail.setSmtpPort(conf.smtpPort)
5657
multiPartMail.setStartTLSEnabled(conf.smtpTls)
57-
multiPartMail.setAuthenticator(new DefaultAuthenticator(conf.smtpUser, conf.smtpPass))
58+
if (conf.smtpAuth) {
59+
multiPartMail.setAuthenticator(new DefaultAuthenticator(conf.smtpUser, conf.smtpPass))
60+
}
5861
multiPartMail.setDebug(false)
5962
multiPartMail.getMailSession.getProperties.put("mail.smtp.ssl.protocols", "TLSv1.2")
6063

@@ -69,17 +72,16 @@ class Mailer(conf: MailerConfig) extends Actor with LazyLogging {
6972
/**
7073
* Extracts an email address from the given string and passes to the enclosed method.
7174
*/
72-
private def setAddress(emailAddress: String)(setter: (String, String) => _) {
75+
private def setAddress(emailAddress: String)(setter: (String, String) => _): Unit =
7376
try {
7477
val iAddress = new InternetAddress(emailAddress)
7578
val address = iAddress.getAddress
7679
val name = iAddress.getPersonal
7780

7881
setter(address, name)
7982
} catch {
80-
case _: Exception => ()
83+
case e: Exception => logger.warn(s"Failed to set email address: $e")
8184
}
82-
}
8385

8486
/**
8587
* Creates an appropriate email object based on the content type.

conf/application.conf

-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ mail {
181181
pass = ""
182182
}
183183
defaultSender = "webKnossos <[email protected]>"
184-
reply = "webKnossos <[email protected]>"
185184
mailchimp {
186185
host = ""
187186
listId = ""

docs/dashboard.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ You can *view* a dataset (read-only) or start new annotations from this screen.
99
Search for your dataset by using the search bar or sorting any of the table columns.
1010
Learn more about managing datasets in the [Datasets guide](./datasets.md).
1111

12-
The presentation differs corresponding to your user role.
12+
The presentation differs depending on your user role.
1313
Regular users can only start or continue annotations and work on tasks.
1414
[Admins and Team Managers](./users.md#access-rights-roles) also have access to additional administration actions, access-rights management, and advanced dataset properties for each dataset.
1515

16+
Read more about the organization of datasets [here](./datasets.md#dataset-organization).
17+
1618
![Dashboard for Team Managers or Admins with access to dataset settings and additional administration actions.](./images/dashboard_datasets.jpeg)
1719
![Dashboard for Regular Users](./images/dashboard_regular_user.jpeg)
1820

docs/datasets.md

+2
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ This is because the access permissions are handled cumulatively.
221221
In addition to the folder organization, datasets can also be tagged.
222222
Use the tags column to do so or select a dataset with a click and use the right sidebar.
223223

224+
To move multiple datasets to a folder at once, you can make use of multi-selection. As in typical file explorers, CTRL + left click adds individual datasets to the current selection. Shift + left click selects a range of datasets.
225+
224226
## Dataset Sharing
225227
Read more in the [Sharing guide](./sharing.md#dataset-sharing)
226228

frontend/javascripts/dashboard/advanced_dataset/dataset_action_view.tsx

+19-2
Original file line numberDiff line numberDiff line change
@@ -252,13 +252,30 @@ const onClearCache = async (
252252

253253
export function getDatasetActionContextMenu({
254254
reloadDataset,
255-
dataset,
255+
datasets,
256256
hideContextMenu,
257257
}: {
258258
reloadDataset: (arg0: APIDatasetId) => Promise<void>;
259-
dataset: APIMaybeUnimportedDataset;
259+
datasets: APIMaybeUnimportedDataset[];
260260
hideContextMenu: () => void;
261261
}) {
262+
if (datasets.length !== 1) {
263+
return (
264+
<Menu
265+
onClick={hideContextMenu}
266+
style={{
267+
borderRadius: 6,
268+
}}
269+
mode="vertical"
270+
>
271+
<Menu.Item key="view" disabled>
272+
No actions available.
273+
</Menu.Item>
274+
</Menu>
275+
);
276+
}
277+
const dataset = datasets[0];
278+
262279
return (
263280
<Menu
264281
onClick={hideContextMenu}

frontend/javascripts/dashboard/advanced_dataset/dataset_table.tsx

+71-19
Original file line numberDiff line numberDiff line change
@@ -59,36 +59,36 @@ type Props = {
5959
reloadDataset: (arg0: APIDatasetId, arg1?: Array<APIMaybeUnimportedDataset>) => Promise<void>;
6060
updateDataset: (arg0: APIDataset) => Promise<void>;
6161
addTagToSearch: (tag: string) => void;
62-
onSelectDataset?: (dataset: APIMaybeUnimportedDataset | null) => void;
63-
selectedDataset?: APIMaybeUnimportedDataset | null | undefined;
62+
onSelectDataset: (dataset: APIMaybeUnimportedDataset | null, multiSelect?: boolean) => void;
63+
selectedDatasets: APIMaybeUnimportedDataset[];
6464
hideDetailsColumns?: boolean;
6565
context: DatasetCacheContextValue | DatasetCollectionContextValue;
6666
};
6767
type State = {
6868
prevSearchQuery: string;
6969
sortedInfo: SorterResult<string>;
7070
contextMenuPosition: [number, number] | null | undefined;
71-
datasetForContextMenu: APIMaybeUnimportedDataset | null;
71+
datasetsForContextMenu: APIMaybeUnimportedDataset[];
7272
};
7373

7474
type ContextMenuProps = {
7575
contextMenuPosition: [number, number] | null | undefined;
7676
hideContextMenu: () => void;
77-
dataset: APIMaybeUnimportedDataset | null;
77+
datasets: APIMaybeUnimportedDataset[];
7878
reloadDataset: Props["reloadDataset"];
7979
};
8080

8181
function ContextMenuInner(propsWithInputRef: ContextMenuProps) {
8282
const inputRef = React.useContext(ContextMenuContext);
83-
const { dataset, reloadDataset, contextMenuPosition, hideContextMenu } = propsWithInputRef;
83+
const { datasets, reloadDataset, contextMenuPosition, hideContextMenu } = propsWithInputRef;
8484
let overlay = <div />;
8585

86-
if (contextMenuPosition != null && dataset != null) {
86+
if (contextMenuPosition != null) {
8787
// getDatasetActionContextMenu should not be turned into <DatasetActionMenu />
8888
// as this breaks antd's styling of the menu within the dropdown.
8989
overlay = getDatasetActionContextMenu({
9090
hideContextMenu,
91-
dataset,
91+
datasets,
9292
reloadDataset,
9393
});
9494
}
@@ -239,8 +239,12 @@ class DatasetTable extends React.PureComponent<Props, State> {
239239
},
240240
prevSearchQuery: "",
241241
contextMenuPosition: null,
242-
datasetForContextMenu: null,
242+
datasetsForContextMenu: [],
243243
};
244+
// currentPageData is only used for range selection (and not during
245+
// rendering). That's why it's not included in this.state (also it
246+
// would lead to infinite loops, too).
247+
currentPageData: APIMaybeUnimportedDataset[] = [];
244248

245249
static getDerivedStateFromProps(nextProps: Props, prevState: State): Partial<State> {
246250
const maybeSortedInfo: SorterResult<string> | {} = // Clear the sorting exactly when the search box is initially filled
@@ -263,7 +267,6 @@ class DatasetTable extends React.PureComponent<Props, State> {
263267
_pagination: TablePaginationConfig,
264268
_filters: Record<string, FilterValue | null>,
265269
sorter: SorterResult<RecordType> | SorterResult<RecordType>[],
266-
_extra: TableCurrentDataSource<RecordType>,
267270
) => {
268271
this.setState({
269272
// @ts-ignore
@@ -407,7 +410,7 @@ class DatasetTable extends React.PureComponent<Props, State> {
407410
hideContextMenu={() => {
408411
this.setState({ contextMenuPosition: null });
409412
}}
410-
dataset={this.state.datasetForContextMenu}
413+
datasets={this.state.datasetsForContextMenu}
411414
reloadDataset={this.props.reloadDataset}
412415
contextMenuPosition={this.state.contextMenuPosition}
413416
/>
@@ -423,7 +426,19 @@ class DatasetTable extends React.PureComponent<Props, State> {
423426
locale={{
424427
emptyText: this.renderEmptyText(),
425428
}}
429+
summary={(currentPageData) => {
430+
// Workaround to get to the currently rendered entries (since the ordering
431+
// is managed by antd).
432+
// Also see https://github.com/ant-design/ant-design/issues/24022.
433+
this.currentPageData = currentPageData as APIMaybeUnimportedDataset[];
434+
return null;
435+
}}
426436
onRow={(record: APIMaybeUnimportedDataset) => ({
437+
onDragStart: () => {
438+
if (!this.props.selectedDatasets.includes(record)) {
439+
this.props.onSelectDataset(record);
440+
}
441+
},
427442
onClick: (event) => {
428443
// @ts-expect-error
429444
if (event.target?.tagName !== "TD") {
@@ -432,11 +447,38 @@ class DatasetTable extends React.PureComponent<Props, State> {
432447
// (e.g., the link action and a (de)selection).
433448
return;
434449
}
435-
if (this.props.onSelectDataset) {
436-
if (this.props.selectedDataset === record) {
437-
this.props.onSelectDataset(null);
438-
} else {
439-
this.props.onSelectDataset(record);
450+
451+
if (!event.shiftKey || this.props.selectedDatasets.length === 0) {
452+
this.props.onSelectDataset(record, event.ctrlKey || event.metaKey);
453+
} else {
454+
// Shift was pressed and there's already another selected dataset that was not
455+
// clicked just now.
456+
// We are using the current page data as there is no way to get the currently
457+
// rendered datasets otherwise. Also see
458+
// https://github.com/ant-design/ant-design/issues/24022.
459+
const renderedDatasets = this.currentPageData;
460+
461+
const clickedDatasetIdx = renderedDatasets.indexOf(record);
462+
const selectedIndices = this.props.selectedDatasets.map((selectedDS) =>
463+
renderedDatasets.indexOf(selectedDS),
464+
);
465+
const closestSelectedDatasetIdx = _.minBy(selectedIndices, (idx) =>
466+
Math.abs(idx - clickedDatasetIdx),
467+
);
468+
469+
if (clickedDatasetIdx == null || closestSelectedDatasetIdx == null) {
470+
return;
471+
}
472+
473+
const [start, end] = [closestSelectedDatasetIdx, clickedDatasetIdx].sort(
474+
(a, b) => a - b,
475+
);
476+
477+
for (let idx = start; idx <= end; idx++) {
478+
// closestSelectedDatasetIdx is already selected (don't deselect it).
479+
if (idx !== closestSelectedDatasetIdx) {
480+
this.props.onSelectDataset(renderedDatasets[idx], true);
481+
}
440482
}
441483
}
442484
},
@@ -466,15 +508,25 @@ class DatasetTable extends React.PureComponent<Props, State> {
466508
const y = event.clientY - bounds.top;
467509

468510
this.showContextMenuAt(x, y);
469-
this.setState({ datasetForContextMenu: record });
511+
if (this.props.selectedDatasets.includes(record)) {
512+
this.setState({
513+
datasetsForContextMenu: this.props.selectedDatasets,
514+
});
515+
} else {
516+
// If dataset is clicked which is not selected, ignore the selected
517+
// datasets.
518+
this.setState({
519+
datasetsForContextMenu: [record],
520+
});
521+
}
470522
},
471523
onDoubleClick: () => {
472524
window.location.href = `/datasets/${record.owningOrganization}/${record.name}/view`;
473525
},
474526
})}
475527
rowSelection={{
476-
selectedRowKeys: this.props.selectedDataset ? [this.props.selectedDataset.name] : [],
477-
onSelectNone: () => this.props.onSelectDataset?.(null),
528+
selectedRowKeys: this.props.selectedDatasets.map((ds) => ds.name),
529+
onSelectNone: () => this.props.onSelectDataset(null),
478530
}}
479531
>
480532
<Column
@@ -644,7 +696,7 @@ export function DatasetTags({
644696
};
645697

646698
return (
647-
<div style={{ maxWidth: 280 }}>
699+
<div className="tags-container">
648700
{dataset.tags.map((tag) => (
649701
<CategorizationLabel
650702
tag={tag}

frontend/javascripts/dashboard/dashboard_view.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,15 @@ class DashboardView extends PureComponent<PropsWithRouter, State> {
327327
}
328328
function DatasetViewWithLegacyContext({ user }: { user: APIUser }) {
329329
const datasetCacheContext = useContext(DatasetCacheContext);
330-
return <DatasetView user={user} hideDetailsColumns={false} context={datasetCacheContext} />;
330+
return (
331+
<DatasetView
332+
user={user}
333+
hideDetailsColumns={false}
334+
context={datasetCacheContext}
335+
selectedDatasets={[]}
336+
onSelectDataset={() => {}}
337+
/>
338+
);
331339
}
332340

333341
const mapStateToProps = (state: OxalisState): StateProps => ({

frontend/javascripts/dashboard/dataset/dataset_collection_context.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export type DatasetCollectionContextValue = {
3838
setActiveFolderId: (id: string | null) => void;
3939
mostRecentlyUsedActiveFolderId: string | null;
4040
supportsFolders: true;
41+
selectedDatasets: APIMaybeUnimportedDataset[];
42+
setSelectedDatasets: React.Dispatch<React.SetStateAction<APIMaybeUnimportedDataset[]>>;
4143
globalSearchQuery: string | null;
4244
setGlobalSearchQuery: (val: string | null) => void;
4345
searchRecursively: boolean;
@@ -83,6 +85,7 @@ export default function DatasetCollectionContextProvider({
8385
const isMutating = useIsMutating() > 0;
8486
const { data: folder } = useFolderQuery(activeFolderId);
8587

88+
const [selectedDatasets, setSelectedDatasets] = useState<APIMaybeUnimportedDataset[]>([]);
8689
const [globalSearchQuery, setGlobalSearchQueryInner] = useState<string | null>(null);
8790
const setGlobalSearchQuery = useCallback(
8891
(value: string | null) => {
@@ -203,6 +206,8 @@ export default function DatasetCollectionContextProvider({
203206

204207
datasetsInFolderQuery.refetch();
205208
},
209+
selectedDatasets,
210+
setSelectedDatasets,
206211
globalSearchQuery,
207212
setGlobalSearchQuery,
208213
searchRecursively,
@@ -238,6 +243,8 @@ export default function DatasetCollectionContextProvider({
238243
updateFolderMutation,
239244
moveFolderMutation,
240245
updateDatasetMutation,
246+
selectedDatasets,
247+
setSelectedDatasets,
241248
globalSearchQuery,
242249
],
243250
);

0 commit comments

Comments
 (0)