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

Adds tag filter #861

Merged
merged 2 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
81 changes: 81 additions & 0 deletions ui/frontend/src/components/common/TagSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import ReactSelect from "react-select";

export const TagSelectorWithValues = (props: {
setSelectedTags: (tags: Map<string, Set<string>>) => void;
allTags: Map<string, Set<string>>; // Map of tag to potential values
selectedTags: Map<string, Set<string>>;
placeholder?: string;
}) => {
// TODO -- get typing correct here
const selectOptions = Array.from(props.allTags)
// This is confusing -- we actually shouldn't have to filter out the already selected ones
// But there seems to be a bug with the ID/uniquifying it so we're doing it manually for now
// TODO -- figure out why we end up doing this
.flatMap(([tag, value]) => {
return Array.from(value)
.sort()
.map((v) => {
if (
props.selectedTags.has(tag) &&
props.selectedTags.get(tag)?.has(v)
) {
return null;
}
return {
value: [tag, v],
label: `${tag}=${v}`,
id: `${tag}${v}`,
};
});
})
.filter((item) => item !== null) as {
value: [string, string];
label: string;
id: string;
}[];

return (
<div className="flex-1"
>
<ReactSelect
onChange={(selected) => {
const selectedTags = new Map<string, Set<string>>();
selected.forEach(({ value }) => {
const [tag, v] = value;
if (!selectedTags.has(tag)) {
selectedTags.set(tag, new Set());
}
selectedTags.get(tag)?.add(v);
});
props.setSelectedTags(selectedTags);
}}
options={selectOptions}
isMulti
className="text-lg sm:text-sm"
placeholder={
props.placeholder !== undefined
? props.placeholder
: "Select tags to view..."
}
value={Array.from(props.selectedTags).flatMap(([tag, values]) => {
return Array.from(values).map((value) => {
return { value: [tag, value], label: `${tag}=${value}` };
});
})}
/>
</div>
);
};

export const selectedTagsToObj = (map: Map<string, Set<string>>): object => {
const obj = Object.fromEntries(
Array.from(map).map(([key, value]) => [key, Array.from(value)])
);
return obj;
};

export const objToSelectedTags = (obj: object): Map<string, Set<string>> => {
return new Map(
Object.entries(obj).map(([key, value]) => [key, new Set(value as string[])])
);
};
96 changes: 84 additions & 12 deletions ui/frontend/src/components/dashboard/Catalog/SearchTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import {
import { NodeRunsView } from "./NodeRunExpansion";
import { getFunctionIdentifier } from "../Code/CodeExplorer";
import { useSearchParams } from "react-router-dom";
import {
TagSelectorWithValues,
selectedTagsToObj,
} from "../../common/TagSelector";

//eslint-disable-next-line @typescript-eslint/no-explicit-any
const displayTagValue = (value: any) => {
Expand Down Expand Up @@ -238,12 +242,52 @@ const extractTableData = (
projectVersionName: "",
projectId: projectId,
codeArtifact: codeArtifactsById.get(node.code_artifacts?.[0]),

// runInfo: [],
};
});
};

type QueryState = {
searchTerm: string;
selectedTags: Map<string, Set<string>>;
};
// TODO -- organize this with the one in RunSummary, there's some duplicated code
const useQueryState = () => {
const [queryParams, setQueryParams] = useSearchParams();
const searchTerm = queryParams.get("query") || "";
const selectedTagsRaw = queryParams.get("tags");
let selectedTags = new Map<string, Set<string>>();
if (selectedTagsRaw !== null) {
const parsed = JSON.parse(selectedTagsRaw);
selectedTags = new Map(
Object.entries(parsed).map(([key, value]) => [
key,
new Set(value as string[]),
])
);
}
const setParams = (params: QueryState) => {
const newParams = new URLSearchParams();
if (params.searchTerm !== "") {
newParams.set("query", params.searchTerm);
}
if (params.selectedTags.size > 0) {
newParams.set(
"tags",
JSON.stringify(selectedTagsToObj(params.selectedTags))
);
}
setQueryParams(newParams);
};
return [
{
searchTerm,
selectedTags,
},
setParams,
] as const;
};

/**
* Table with search bar for nodes in prior versions/runs
* Note that this optionally comes with the ability to attach run data to it -- that will augment
Expand All @@ -256,11 +300,8 @@ export const CatalogView: FC<{
}> = (props) => {
const projectId = props.project.id as number;
const catalogData = useCatalogView({ projectId: projectId, limit: 10000 });
const [queryParams, setQueryParams] = useSearchParams();
const searchTerm = queryParams.get("query") || "";
const setSearchTerm = (term: string) => {
setQueryParams({ query: term });
};

const [{ searchTerm, selectedTags }, setQueryParams] = useQueryState();
const [expandedRowsByKey, setExpandedRowsByKey] = useState<
Map<string, "runtime-chart" | "table">
>(new Map());
Expand All @@ -269,6 +310,15 @@ export const CatalogView: FC<{
catalogData.data?.code_artifacts || [],
props.project.id as number
);
const allTags = new Map<string, Set<string>>();
searchData.forEach((row) => {
Object.entries(row.node.tags || {}).forEach(([tag, value]) => {
if (!allTags.has(tag)) {
allTags.set(tag, new Set());
}
allTags.get(tag)?.add(value as string);
});
});
const fuse = useMemo(
() =>
new Fuse(searchData, {
Expand Down Expand Up @@ -366,14 +416,23 @@ export const CatalogView: FC<{
];

const shouldDisplay = (row: { item: RowToDisplay }) => {
// return true;
//eslint-disable-next-line no-prototype-builtins
const node = row.item.node;
return !Object.prototype.hasOwnProperty.call(
let disp = true;
disp &&= !Object.prototype.hasOwnProperty.call(
node.tags,
"hamilton.non_final_node"
);
// return !node.tags.hasOwnProperty("hamilton.decorators.non_final"); // Really it should check the value...
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tags = node.tags as any;
if (selectedTags.size > 0) {
for (const [tag, values] of Array.from(selectedTags.entries())) {
if (!values.has(tags[tag])) {
disp = false;
break;
}
}
}
return disp;
};
const getSearchResult = (term: string) => {
if (searchTerm == "") {
Expand All @@ -394,8 +453,21 @@ export const CatalogView: FC<{
<div className="px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center sticky top-0 bg-white z-50 px-5">
<div className="w-full">
<div className="pt-10 pb-10">
<FeatureSearchBox setSearch={setSearchTerm} term={searchTerm} />
<div className="pt-10 pb-10 flex flex-col gap-2">
<FeatureSearchBox
setSearch={(term) => {
setQueryParams({ searchTerm: term, selectedTags });
}}
term={searchTerm}
/>
<TagSelectorWithValues
selectedTags={selectedTags}
setSelectedTags={(tags) => {
setQueryParams({ searchTerm, selectedTags: tags });
}}
allTags={allTags}
placeholder="Filter by tags (select key/value pairs, multiple values is an OR query)"
/>
</div>
</div>
</div>
Expand Down
28 changes: 20 additions & 8 deletions ui/frontend/src/components/dashboard/NavBreadCrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChevronRightIcon, HomeIcon } from "@heroicons/react/20/solid";
import { DAGTemplateWithoutData, Project } from "../../state/api/friendlyApi";
import { Link } from "react-router-dom";

export const NavBreadCrumb = (props: {
project: Project;
Expand All @@ -18,19 +19,27 @@ export const NavBreadCrumb = (props: {
: index == 3
? props.dagTemplates[0]?.name
: pathName;
let linkPath =
"/dashboard/" +
pathNameRelativeToDashboard.slice(0, index + 1).join("/");
if (linkPath.endsWith("/project")) {
linkPath = "/dashboard/projects";
} else if (linkPath.endsWith("/version")) {
linkPath = linkPath.replace("/version", "/versions");
}
return (
// TODO -- enable when we can get this working -- we just need to link for links that exist
// Should be easy I just don't have time now
<span
// to={
// "/dashboard/" +
// pathNameRelativeToDashboard.slice(0, index + 1).join("/")
// }
<Link
to={
"/dashboard/" +
pathNameRelativeToDashboard.slice(0, index + 1).join("/")
}
key={linkName}
className="ml-4 font-medium hover:text-gray-800"
className="ml-4 font-medium hover:text-gray-800 cursor-pointer hover:scale-105"
>
{linkName}
</span>
</Link>
);
}),
];
Expand All @@ -41,7 +50,10 @@ export const NavBreadCrumb = (props: {
const elements = getElements();

return (
<nav className="flex max-w-full bg-transparent" aria-label="Breadcrumb">
<nav
className="flex max-w-full bg-transparent z-50"
aria-label="Breadcrumb"
>
<ol role="list" className="flex items-center space-x-4 flex-wrap">
<li>
<div>
Expand Down
Loading
Loading