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

Implement folder specific search for datasets #6677

Merged
merged 32 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1ea19bc
fix invalid active folder when switching to another organization
philippotto Nov 30, 2022
3db78db
adapt wording in sidebar when SEARCH_RESULTS_LIMIT is exceeded
philippotto Nov 30, 2022
c0d0e6e
dont create folder when user hit escape in new-name-prompt
philippotto Dec 1, 2022
8a3e2f4
rename 'default' to Administrators & Dataset Managers
philippotto Dec 1, 2022
fe54b9d
show drag preview image when dragging a dataset row
philippotto Dec 1, 2022
94848bc
add file icons
philippotto Dec 1, 2022
16d46ca
extend comment
philippotto Dec 1, 2022
76eb582
don't select a folder when an entry of its context menu was selected
philippotto Dec 1, 2022
7276b10
select folder root when deleting active folder
philippotto Dec 1, 2022
0a606bd
update changelog
philippotto Dec 1, 2022
cd9711c
fix default value when setting recommended settings for task type
philippotto Dec 1, 2022
10d195b
implement folder-specific search
philippotto Dec 2, 2022
17e09a8
implement recursive folder search in backend and adapt frontend accor…
philippotto Dec 2, 2022
7285fd7
format
philippotto Dec 2, 2022
e950557
only show breadcrumbs OR datastore in dataset table
philippotto Dec 2, 2022
43a6d9b
improve UI for recursive configuration of search
philippotto Dec 5, 2022
d014011
Merge branch 'master' of github.com:scalableminds/webknossos into fol…
philippotto Dec 5, 2022
3466d40
update changelog
philippotto Dec 5, 2022
cee5f02
add folder icon to breadcrumb tag
philippotto Dec 5, 2022
701a2e9
invalidate search results when mutating a dataset
philippotto Dec 5, 2022
f946290
change search logic to implement three cases (global, folder, recursi…
philippotto Dec 6, 2022
bef4bc7
get rid of folderIdForSearch by unifying it with activeFolderId
philippotto Dec 7, 2022
f863eba
remember most recently used folder so that that folder can be re-acti…
philippotto Dec 7, 2022
2cc4318
clean up
philippotto Dec 7, 2022
edcb920
use fancy-schmancy match expression
philippotto Dec 7, 2022
cbb6ea0
Merge branch 'master' into folder-specific-search
philippotto Dec 7, 2022
e89b7a0
rename recursive to includeSubfolders
philippotto Dec 7, 2022
ab26e26
rename falsy to nullable in usePrevious
philippotto Dec 7, 2022
3b7db93
fix indexing of empty array and rename ignoreNullableValue to ignoreN…
philippotto Dec 7, 2022
3f29d02
fix get new task button in task tab in dashboard
philippotto Dec 8, 2022
2ae8674
update changelog
philippotto Dec 8, 2022
671abc9
Merge branch 'master' into folder-specific-search
philippotto Dec 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released

### Added
- 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)
- Added the option to search a specific folder in the new datasets tab. [#6677](https://github.com/scalableminds/webknossos/pull/6677)

### Changed
- webKnossos is now able to recover from a lost webGL context. [#6663](https://github.com/scalableminds/webknossos/pull/6663)
Expand Down
7 changes: 6 additions & 1 deletion app/controllers/DataSetController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ class DataSetController @Inject()(userService: UserService,
uploaderId: Option[String],
@ApiParam(value = "Optional filtering: List only datasets in the folder with this id")
folderId: Option[String],
@ApiParam(
value =
"Optional filtering: If a folderId was specified, this parameter controls whether subfolders should be considered, too (default: false)")
recursive: Option[Boolean],
fm3 marked this conversation as resolved.
Show resolved Hide resolved
@ApiParam(value = "Optional filtering: List only datasets with names matching this search query")
searchQuery: Option[String],
@ApiParam(value = "Optional limit, return only the first n matching datasets.")
Expand Down Expand Up @@ -210,7 +214,8 @@ class DataSetController @Inject()(userService: UserService,
) { filter =>
for {
folderIdValidated <- Fox.runOptional(folderId)(ObjectId.fromString)
dataSets <- dataSetDAO.findAllWithSearch(folderIdValidated, searchQuery) ?~> "dataSet.list.failed"
dataSets <- dataSetDAO
.findAllWithSearch(folderIdValidated, searchQuery, recursive.getOrElse(false)) ?~> "dataSet.list.failed"
filtered <- filter.applyOn(dataSets)
limited = limit.map(l => filtered.take(l)).getOrElse(filtered)
js <- listGrouped(limited, request.identity) ?~> "dataSet.list.failed"
Expand Down
11 changes: 9 additions & 2 deletions app/models/binary/DataSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,18 @@ class DataSetDAO @Inject()(sqlClient: SQLClient,
parsed <- parseFirst(r, id)
} yield parsed

def findAllWithSearch(folderIdOpt: Option[ObjectId], searchQuery: Option[String])(
def findAllWithSearch(folderIdOpt: Option[ObjectId], searchQuery: Option[String], recursive: Boolean = false)(
implicit ctx: DBAccessContext): Fox[List[DataSet]] =
for {
accessQuery <- readAccessQuery
folderPredicate = folderIdOpt.map(folderId => s"_folder = '$folderId'").getOrElse("true")
folderPredicate = folderIdOpt
.map(
folderId =>
if (recursive)
fm3 marked this conversation as resolved.
Show resolved Hide resolved
s"_folder in (select _descendant FROM webknossos.folder_paths fp WHERE fp._ancestor = '$folderId')"
philippotto marked this conversation as resolved.
Show resolved Hide resolved
else
s"_folder = '$folderId'")
.getOrElse("true")
searchPredicate = buildSearchPredicate(searchQuery)
r <- run(sql"""SELECT #$columns
FROM #$existingCollectionName
Expand Down
2 changes: 1 addition & 1 deletion conf/webknossos.latest.routes
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ GET /teams/:id/progressOverview
# DataSets
POST /datasets/:organizationName/:dataSetName/createExplorational controllers.AnnotationController.createExplorational(organizationName: String, dataSetName: String)
GET /datasets/:organizationName/:dataSetName/sandbox/:typ controllers.AnnotationController.getSandbox(organizationName: String, dataSetName: String, typ: String, sharingToken: Option[String])
GET /datasets controllers.DataSetController.list(isActive: Option[Boolean], isUnreported: Option[Boolean], isEditable: Option[Boolean], organizationName: Option[String], onlyMyOrganization: Option[Boolean], uploaderId: Option[String], folderId: Option[String], searchQuery: Option[String], limit: Option[Int])
GET /datasets controllers.DataSetController.list(isActive: Option[Boolean], isUnreported: Option[Boolean], isEditable: Option[Boolean], organizationName: Option[String], onlyMyOrganization: Option[Boolean], uploaderId: Option[String], folderId: Option[String], recursive: Option[Boolean], searchQuery: Option[String], limit: Option[Int])
POST /datasets controllers.DataSetController.create(typ: String)
POST /datasets/exploreRemote controllers.DataSetController.exploreRemoteDataset
GET /datasets/disambiguate/:dataSetName/toNew controllers.DataSetController.getOrganizationForDataSet(dataSetName: String)
Expand Down
6 changes: 5 additions & 1 deletion frontend/javascripts/admin/admin_rest_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,13 +1025,14 @@ export async function getDatasets(
isUnreported: boolean | null | undefined = null,
folderId: string | null = null,
searchQuery: string | null = null,
searchRecursively: boolean | null = null,
limit: number | null = null,
): Promise<Array<APIMaybeUnimportedDataset>> {
const params = new URLSearchParams();
if (isUnreported != null) {
params.append("isUnreported", String(isUnreported));
}
if (folderId != null) {
if (folderId != null && folderId !== "") {
params.append("folderId", folderId);
}
if (searchQuery != null) {
Expand All @@ -1040,6 +1041,9 @@ export async function getDatasets(
if (limit != null) {
params.append("limit", String(limit));
}
if (searchRecursively != null) {
params.append("recursive", searchRecursively ? "true" : "false");
}

const datasets = await Request.receiveJSON(`/api/datasets?${params}`);
assertResponseLimit(datasets);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ import { ContextMenuContext, GenericContextMenuContainer } from "oxalis/view/con
import Shortcut from "libs/shortcut_component";
import { MINIMUM_SEARCH_QUERY_LENGTH } from "dashboard/dataset/queries";
import { useSelector } from "react-redux";
import { DatasetCacheContextValue } from "dashboard/dataset/dataset_cache_provider";
import { DatasetCollectionContextValue } from "dashboard/dataset/dataset_collection_context";
import { Unicode } from "oxalis/constants";

const { ThinSpace } = Unicode;
const { Column } = Table;
const typeHint: APIMaybeUnimportedDataset[] = [];
const useLruRank = true;
Expand All @@ -58,6 +62,7 @@ type Props = {
onSelectDataset?: (dataset: APIMaybeUnimportedDataset | null) => void;
selectedDataset?: APIMaybeUnimportedDataset | null | undefined;
hideDetailsColumns?: boolean;
context: DatasetCacheContextValue | DatasetCollectionContextValue;
};
type State = {
prevSearchQuery: string;
Expand Down Expand Up @@ -489,7 +494,12 @@ class DatasetTable extends React.PureComponent<Props, State> {
{dataset.name}
</Link>
<br />
<Tag color={stringToColor(dataset.dataStore.name)}>{dataset.dataStore.name}</Tag>

{"getBreadcrumbs" in this.props.context ? (
<BreadcrumbsTag parts={this.props.context.getBreadcrumbs(dataset)} />
) : (
<Tag color={stringToColor(dataset.dataStore.name)}>{dataset.dataStore.name}</Tag>
)}
</>
)}
/>
Expand Down Expand Up @@ -725,4 +735,26 @@ export function TeamTags({
);
}

function formatPath(parts: string[]) {
return parts.join(`${ThinSpace}/${ThinSpace}`);
}

function BreadcrumbsTag({ parts: allParts }: { parts: string[] | null }) {
if (allParts == null) {
return null;
}
let parts;
if (allParts.length <= 4) {
parts = allParts;
} else {
parts = [...allParts.slice(0, 2), "...", ...allParts.slice(-2)];
}

return (
<Tooltip title={`This folder is located in ${formatPath(allParts)}.`}>
philippotto marked this conversation as resolved.
Show resolved Hide resolved
<Tag>{formatPath(parts)}</Tag>
</Tooltip>
);
}

export default DatasetTable;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getDatastores, triggerDatasetCheck } from "admin/admin_rest_api";
import UserLocalStorage from "libs/user_local_storage";
import _ from "lodash";
import {
useFolderTreeQuery,
useFolderHierarchyQuery,
useDatasetsInFolderQuery,
useDatasetSearchQuery,
useCreateFolderMutation,
Expand Down Expand Up @@ -38,8 +38,13 @@ export type DatasetCollectionContextValue = {
supportsFolders: true;
globalSearchQuery: string | null;
setGlobalSearchQuery: (val: string | null) => void;
folderIdForSearch: string | null;
setFolderIdForSearch: (val: string | null) => void;
searchRecursively: boolean;
setSearchRecursively: (val: boolean) => void;
getBreadcrumbs: (dataset: APIMaybeUnimportedDataset) => string[] | null;
queries: {
folderTreeQuery: ReturnType<typeof useFolderTreeQuery>;
folderHierarchyQuery: ReturnType<typeof useFolderHierarchyQuery>;
datasetsInFolderQuery: ReturnType<typeof useDatasetsInFolderQuery>;
datasetSearchQuery: ReturnType<typeof useDatasetSearchQuery>;
createFolderMutation: ReturnType<typeof useCreateFolderMutation>;
Expand Down Expand Up @@ -78,6 +83,7 @@ export default function DatasetCollectionContextProvider({
const { data: folder } = useFolderQuery(activeFolderId);

const [globalSearchQuery, setGlobalSearchQueryInner] = useState<string | null>(null);
const [folderIdForSearch, setFolderIdForSearch] = useState<string | null>(null);
const setGlobalSearchQuery = useCallback(
(value: string | null) => {
// Empty string should be handled as null
Expand All @@ -88,6 +94,7 @@ export default function DatasetCollectionContextProvider({
},
[setGlobalSearchQueryInner, setActiveFolderId],
);
const [searchRecursively, setSearchRecursively] = useState<boolean>(true);

// Clear search query if active folder changes.
useEffect(() => {
Expand All @@ -97,7 +104,14 @@ export default function DatasetCollectionContextProvider({
}, [activeFolderId]);

// Keep url GET parameters in sync with search and active folder
useManagedUrlParams(setGlobalSearchQuery, setActiveFolderId, globalSearchQuery, activeFolderId);
useManagedUrlParams(
setGlobalSearchQuery,
setActiveFolderId,
globalSearchQuery,
activeFolderId,
folderIdForSearch,
setFolderIdForSearch,
);

useEffect(() => {
// Persist last active folder to localStorage. We
Expand All @@ -109,9 +123,13 @@ export default function DatasetCollectionContextProvider({
}
}, [folder, activeFolderId]);

const folderTreeQuery = useFolderTreeQuery();
const folderHierarchyQuery = useFolderHierarchyQuery();
const datasetsInFolderQuery = useDatasetsInFolderQuery(activeFolderId);
const datasetSearchQuery = useDatasetSearchQuery(globalSearchQuery);
const datasetSearchQuery = useDatasetSearchQuery(
globalSearchQuery,
folderIdForSearch,
searchRecursively,
);
const createFolderMutation = useCreateFolderMutation();
const deleteFolderMutation = useDeleteFolderMutation();
const updateFolderMutation = useUpdateFolderMutation();
Expand All @@ -134,10 +152,26 @@ export default function DatasetCollectionContextProvider({
updateDatasetMutation.mutateAsync([dataset, dataset.folderId]);
}

const getBreadcrumbs = (dataset: APIMaybeUnimportedDataset) => {
if (folderHierarchyQuery.data?.itemById == null) {
return null;
}
const { itemById } = folderHierarchyQuery.data;

let currentFolder = itemById[dataset.folderId];
const breadcrumbs = [currentFolder.title];
while (currentFolder?.parent != null) {
currentFolder = itemById[currentFolder.parent];
breadcrumbs.unshift(currentFolder.title);
}

return breadcrumbs;
};

const isLoading =
(globalSearchQuery
? datasetSearchQuery.isFetching
: folderTreeQuery.isLoading ||
: folderHierarchyQuery.isLoading ||
datasetsInFolderQuery.isFetching ||
datasetsInFolderQuery.isRefetching) || isMutating;

Expand All @@ -152,6 +186,7 @@ export default function DatasetCollectionContextProvider({
activeFolderId,
setActiveFolderId,
isChecking,
getBreadcrumbs,
checkDatasets: async () => {
if (isChecking) {
console.warn("Ignore second rechecking request, since a recheck is already in progress");
Expand All @@ -175,8 +210,12 @@ export default function DatasetCollectionContextProvider({
},
globalSearchQuery,
setGlobalSearchQuery,
folderIdForSearch,
setFolderIdForSearch,
searchRecursively,
setSearchRecursively,
queries: {
folderTreeQuery,
folderHierarchyQuery,
datasetsInFolderQuery,
datasetSearchQuery,
createFolderMutation,
Expand All @@ -195,14 +234,19 @@ export default function DatasetCollectionContextProvider({
updateCachedDataset,
activeFolderId,
setActiveFolderId,
folderTreeQuery,
folderHierarchyQuery,
datasetsInFolderQuery,
datasetSearchQuery,
searchRecursively,
setSearchRecursively,
createFolderMutation,
deleteFolderMutation,
updateFolderMutation,
moveFolderMutation,
updateDatasetMutation,
globalSearchQuery,
folderIdForSearch,
setFolderIdForSearch,
],
);

Expand All @@ -216,6 +260,8 @@ function useManagedUrlParams(
setActiveFolderId: React.Dispatch<React.SetStateAction<string | null>>,
globalSearchQuery: string | null,
activeFolderId: string | null,
folderIdForSearch: string | null,
setFolderIdForSearch: React.Dispatch<React.SetStateAction<string | null>>,
) {
const { data: folder } = useFolderQuery(activeFolderId);

Expand All @@ -226,6 +272,10 @@ function useManagedUrlParams(
if (query) {
setGlobalSearchQuery(query);
}
const folderId = params.get("folderId");
if (folderId) {
setFolderIdForSearch(folderId);
}

const folderSpecifier = _.last(location.pathname.split("/"));

Expand All @@ -238,14 +288,19 @@ function useManagedUrlParams(
}
}, []);

// Update query
// Update query and folderIdForSearch
useEffect(() => {
const params = new URLSearchParams(location.search);
if (globalSearchQuery) {
params.set("query", globalSearchQuery);
} else {
params.delete("query");
}
if (globalSearchQuery && folderIdForSearch) {
params.set("folderId", folderIdForSearch);
} else {
params.delete("folderId");
}
const paramStr = params.toString();

// Don't use useHistory because this would lose the input search
Expand All @@ -255,7 +310,7 @@ function useManagedUrlParams(
"",
`${location.pathname}${paramStr === "" ? "" : "?"}${paramStr}`,
);
}, [globalSearchQuery]);
}, [globalSearchQuery, folderIdForSearch]);

// Update folderId
useEffect(() => {
Expand Down
Loading