This is an automated email from the ASF dual-hosted git repository.
pawarprasad123 pushed a commit to branch ATLAS-5246-v1
in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/ATLAS-5246-v1 by this push:
new bad84f1dc ATLAS-5251: ATLAS UI : Dashboard - Latest Entities List
(#619)
bad84f1dc is described below
commit bad84f1dc05ba4084722fbc49ae5732cdf954188
Author: Prasad Pawar <[email protected]>
AuthorDate: Fri May 8 12:57:40 2026 +0530
ATLAS-5251: ATLAS UI : Dashboard - Latest Entities List (#619)
---
dashboard/src/api/apiMethods/searchApiMethod.ts | 62 ++++-
dashboard/src/utils/dashboardSearchUtils.ts | 189 ++++++++++++++
.../views/DashboardOverview/LatestEntitiesList.tsx | 273 +++++++++++++++++++++
.../DashboardOverview/LatestEntitiesSkeleton.tsx | 53 ++++
.../DashboardOverview/latestEntitiesList.utils.ts | 64 +++++
dashboard/src/views/SearchResult/SearchResult.tsx | 24 +-
6 files changed, 658 insertions(+), 7 deletions(-)
diff --git a/dashboard/src/api/apiMethods/searchApiMethod.ts
b/dashboard/src/api/apiMethods/searchApiMethod.ts
index 11a061e4f..8cc58f041 100644
--- a/dashboard/src/api/apiMethods/searchApiMethod.ts
+++ b/dashboard/src/api/apiMethods/searchApiMethod.ts
@@ -56,10 +56,62 @@ const getRelationShipV2 = (params: { params: Record<string,
unknown> }) => {
return fetchApi(url, { method: "GET" });
};
+/** Atlas `SearchParameters.sortBy` / `sortOrder` (see `SortOrder` enum:
DESCENDING). */
+const LATEST_ENTITIES_TIMESTAMP_SORT = "__timestamp" as const;
+
+type LatestEntitiesSearchOptions = {
+ limit: number;
+ includeSubClassifications: boolean;
+};
+
+/**
+ * Request `__timestamp` so sort + “Created … ago” work. Do not set
+ * `excludeHeaderAttributes`: for `_ALL_ENTITY_TYPES`, Atlas validates each
+ * `attributes` entry against `__ENTITY_ROOT` and rejects `name` /
`qualifiedName`
+ * / `guid` (see `excludeHeaderAttributesAllEntityType` in Atlas tests). Normal
+ * headers then include name, guid, typeName like the main basic search.
+ */
+const buildLatestEntitiesBasicBody = (opts: LatestEntitiesSearchOptions) => {
+ return {
+ typeName: "_ALL_ENTITY_TYPES",
+ excludeDeletedEntities: true,
+ includeClassificationAttributes: true,
+ includeSubTypes: true,
+ includeSubClassifications: opts.includeSubClassifications,
+ limit: opts.limit,
+ offset: 0,
+ tagFilters: null,
+ entityFilters: null,
+ classification: null,
+ termName: null,
+ relationshipFilters: null,
+ attributes: ["__timestamp"],
+ sortBy: LATEST_ENTITIES_TIMESTAMP_SORT,
+ sortOrder: "DESCENDING",
+ };
+};
+
+/**
+ * Dashboard card only: newest entities by `__timestamp`, no entity filters,
+ * sub-classifications off, full entity headers for name/guid/type.
+ */
+const getLatestEntities = () => {
+ return getBasicSearchResult(
+ {
+ data: buildLatestEntitiesBasicBody({
+ limit: 7,
+ includeSubClassifications: false,
+ }),
+ },
+ "basic"
+ );
+};
+
export {
- getBasicSearchResult,
- getRelationShipResult,
- getGlobalSearchResult,
- getRelationShip,
- getRelationShipV2
+ getBasicSearchResult,
+ getRelationShipResult,
+ getGlobalSearchResult,
+ getRelationShip,
+ getRelationShipV2,
+ getLatestEntities
};
diff --git a/dashboard/src/utils/dashboardSearchUtils.ts
b/dashboard/src/utils/dashboardSearchUtils.ts
new file mode 100644
index 000000000..15ebdf699
--- /dev/null
+++ b/dashboard/src/utils/dashboardSearchUtils.ts
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { NavigateFunction } from "react-router-dom";
+import { attributeFilter } from "@utils/CommonViewFunction";
+
+export type DashboardSearchType =
+ | "all_entities"
+ | "all_classifications"
+ | "entity_status"
+ | "entity_type";
+
+export interface DashboardSearchParams {
+ type?: string;
+ tag?: string;
+ includeDE?: boolean;
+ entityFilters?: {
+ condition: string;
+ criterion: Array<{ attributeName: string; operator: string;
attributeValue: string }>;
+ };
+}
+
+const buildSearchParams = (
+ type: DashboardSearchType,
+ extra?: Partial<DashboardSearchParams>
+): URLSearchParams => {
+ const params = new URLSearchParams();
+ params.set("searchType", "basic");
+
+ if (type === "all_classifications") {
+ params.set("tag", extra?.tag ?? "_ALL_CLASSIFICATION_TYPES");
+ params.set("type", "");
+ } else {
+ params.set("type", extra?.type ?? "_ALL_ENTITY_TYPES");
+ }
+ if (extra?.includeDE) {
+ params.set("includeDE", "true");
+ }
+ if (extra?.entityFilters) {
+ const urlStr = attributeFilter.generateUrl({ value:
extra.entityFilters });
+ if (urlStr) params.set("entityFilters", urlStr);
+ }
+ return params;
+};
+
+export const navigateToTaggedSearch = (navigate: NavigateFunction): void => {
+ navigateToSearch(navigate, "all_classifications");
+};
+
+export const navigateToClassificationSearch = (
+ navigate: NavigateFunction,
+ tagName: string
+): void => {
+ navigateToSearch(navigate, "all_classifications", { tag: tagName });
+};
+
+/** "View All" from Latest Entities card: basic all-types search, newest
first. */
+export const navigateToLatestEntitiesSearch = (navigate: NavigateFunction):
void => {
+ const params = buildSearchParams("all_entities");
+ params.set("pageLimit", "25");
+ params.set("pageOffset", "0");
+ params.set("sortBy", "__timestamp");
+ params.set("sortOrder", "DESCENDING");
+ /* Same as dashboard card: request system create time in the
basic-search body. */
+ params.set("attributes", "__timestamp");
+ navigate({ pathname: "/search/searchResult", search: params.toString()
});
+};
+
+export const navigateToEntityTypeSearch = (
+ navigate: NavigateFunction,
+ typeName: string,
+ includeDeleted: boolean
+): void => {
+ const extra: Partial<DashboardSearchParams> = {
+ type: typeName,
+ includeDE: includeDeleted
+ };
+ if (includeDeleted) {
+ extra.entityFilters = {
+ condition: "AND",
+ criterion: [{ attributeName: "__state", operator: "eq",
attributeValue: "DELETED" }]
+ };
+ }
+ navigateToSearch(navigate, "entity_type", extra);
+};
+
+/** Search all entity typedefs that belong to one service-type bucket (sidebar
grouping). */
+export const navigateToServiceTypeEntitySearch = (
+ navigate: NavigateFunction,
+ typeNames: string[],
+ includeDeleted: boolean
+): void => {
+ const cleaned = [...new Set(typeNames.filter(Boolean))].sort((a, b) =>
a.localeCompare(b));
+ if (cleaned.length === 0) {
+ navigateToSearch(navigate, "all_entities");
+ return;
+ }
+ if (cleaned.length === 1) {
+ navigateToEntityTypeSearch(navigate, cleaned[0],
includeDeleted);
+ return;
+ }
+ const criterion = cleaned.map((typeName) => ({
+ attributeName: "__typeName",
+ operator: "eq",
+ attributeValue: typeName,
+ }));
+ navigateToSearch(navigate, "all_entities", {
+ type: "_ALL_ENTITY_TYPES",
+ includeDE: includeDeleted,
+ entityFilters: { condition: "OR", criterion },
+ });
+};
+
+export const navigateToSearch = (
+ navigate: NavigateFunction,
+ type: DashboardSearchType,
+ extra?: Partial<DashboardSearchParams>
+): void => {
+ const search = buildSearchParams(type, extra).toString();
+ navigate({ pathname: "/search/searchResult", search });
+};
+
+/** Basic search using free-text `query` (e.g. entity typedef name). */
+export const navigateToBasicTextQuery = (
+ navigate: NavigateFunction,
+ query: string
+): void => {
+ const params = new URLSearchParams();
+ params.set("searchType", "basic");
+ params.set("query", query.trim());
+ params.set("pageLimit", "25");
+ params.set("pageOffset", "0");
+ navigate({ pathname: "/search/searchResult", search: params.toString()
});
+};
+
+export const navigateToClassificationDetailPage = (
+ navigate: NavigateFunction,
+ classificationName: string
+): void => {
+ const sp = new URLSearchParams();
+ sp.set("tag", classificationName);
+ navigate({
+ pathname:
`/tag/tagAttribute/${encodeURIComponent(classificationName)}`,
+ search: sp.toString()
+ });
+};
+
+export const navigateToGlossaryTermDetailPage = (
+ navigate: NavigateFunction,
+ args: {
+ termGuid: string;
+ termId: string;
+ glossaryGuid: string;
+ parentName: string;
+ }
+): void => {
+ const sp = new URLSearchParams();
+ sp.set("gid", args.glossaryGuid);
+ sp.set("term", `${args.termId}@${args.parentName}`);
+ sp.set("gtype", "term");
+ sp.set("viewType", "term");
+ sp.set("guid", args.termGuid);
+ sp.set("searchType", "basic");
+ navigate({
+ pathname: `/glossary/${args.termGuid}`,
+ search: sp.toString()
+ });
+};
+
+export const navigateToBusinessMetadataDetailPage = (
+ navigate: NavigateFunction,
+ bmGuid: string
+): void => {
+ navigate({ pathname: `/administrator/businessMetadata/${bmGuid}` });
+};
diff --git a/dashboard/src/views/DashboardOverview/LatestEntitiesList.tsx
b/dashboard/src/views/DashboardOverview/LatestEntitiesList.tsx
new file mode 100644
index 000000000..2d80a5a1f
--- /dev/null
+++ b/dashboard/src/views/DashboardOverview/LatestEntitiesList.tsx
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { memo, useCallback } from "react";
+import { Paper, Stack, Typography, Link, List, ListItem, Box } from
"@mui/material";
+import { Link as RouterLink } from "react-router-dom";
+import moment from "moment";
+import { useNavigate } from "react-router-dom";
+import { navigateToLatestEntitiesSearch } from "@utils/dashboardSearchUtils";
+import type { LatestEntityRowModel } from "./latestEntitiesList.utils";
+import {
+ resolveLatestEntityDisplayName,
+ resolveLatestEntityGuid,
+ resolveLatestEntityTypeName
+} from "./latestEntitiesList.utils";
+
+interface EntityItem extends LatestEntityRowModel {
+ createTime?: number | Date | string;
+ attributes?: LatestEntityRowModel["attributes"] & {
+ __timestamp?: number | string | Record<string, unknown>;
+ createTime?: number | string;
+ };
+}
+
+interface LatestEntitiesListProps {
+ entities: EntityItem[];
+ isLoading?: boolean;
+ error?: string | null;
+}
+
+const INVALID_TS_LABEL = "Created today";
+
+const unwrapLongLike = (raw: unknown): unknown => {
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw))
return raw;
+ const o = raw as Record<string, unknown>;
+ if (typeof o.$numberLong === "string" || typeof o.$numberLong ===
"number") {
+ return o.$numberLong;
+ }
+ if (typeof o.longValue === "string" || typeof o.longValue === "number")
{
+ return o.longValue;
+ }
+ return raw;
+};
+
+/**
+ * Milliseconds since epoch, or null only when missing / unusable.
+ * Rejects 0 (epoch) to avoid "56 years ago". Accepts sec or ms numbers, ISO
strings.
+ */
+const normalizeEntityTimestampMs = (raw: unknown): number | null => {
+ const v = unwrapLongLike(raw);
+ if (v == null) return null;
+ if (v instanceof Date) {
+ const t = v.getTime();
+ if (!Number.isFinite(t) || t <= 0) return null;
+ return moment(t).isValid() ? t : null;
+ }
+ if (typeof v === "string") {
+ const trimmed = v.trim();
+ if (trimmed === "") return null;
+ const n = Number(trimmed);
+ if (Number.isFinite(n) && n > 0) {
+ const ms = n < 1e12 ? n * 1000 : n;
+ if (moment(ms).isValid() && ms > 0) return ms;
+ }
+ const parsed = moment(trimmed);
+ if (parsed.isValid()) {
+ const ms = parsed.valueOf();
+ if (ms > 0) return ms;
+ }
+ return null;
+ }
+ if (typeof v === "number") {
+ if (!Number.isFinite(v) || v <= 0) return null;
+ const ms = v < 1e12 ? v * 1000 : v;
+ return moment(ms).isValid() && ms > 0 ? ms : null;
+ }
+ return null;
+};
+
+/** Use only `__timestamp` for relative "Created … ago" (not `createTime`,
often 0 in API). */
+const getEntityTimestampRawForDisplay = (entity: EntityItem): unknown => {
+ const a = entity.attributes;
+ const top = entity as EntityItem & { __timestamp?: unknown };
+ return a?.__timestamp ?? top.__timestamp;
+};
+
+/** Valid timestamp only: seconds / minutes / hours for last 24h, then moment
relative. */
+const formatCreatedRelativeFromMs = (ms: number): string => {
+ const now = Date.now();
+ const deltaMs = now - ms;
+ if (!Number.isFinite(deltaMs)) return INVALID_TS_LABEL;
+ if (deltaMs < 0) {
+ return `Created ${moment(ms).fromNow()}`;
+ }
+ const totalSec = Math.floor(deltaMs / 1000);
+ if (totalSec < 1) {
+ return "Created just now";
+ }
+ if (totalSec < 60) {
+ return totalSec === 1
+ ? "Created 1 second ago"
+ : `Created ${totalSec} seconds ago`;
+ }
+ const totalMin = Math.floor(totalSec / 60);
+ if (totalMin < 60) {
+ return totalMin === 1
+ ? "Created 1 minute ago"
+ : `Created ${totalMin} minutes ago`;
+ }
+ const totalHr = Math.floor(totalMin / 60);
+ if (totalHr < 24) {
+ return totalHr === 1
+ ? "Created 1 hour ago"
+ : `Created ${totalHr} hours ago`;
+ }
+ return `Created ${moment(ms).fromNow()}`;
+};
+
+const formatRelativeTime = (raw: unknown): string => {
+ const ms = normalizeEntityTimestampMs(raw);
+ if (ms == null) return INVALID_TS_LABEL;
+ return formatCreatedRelativeFromMs(ms);
+};
+
+const LatestEntitiesList = memo(({ entities, isLoading, error }:
LatestEntitiesListProps) => {
+ const navigate = useNavigate();
+
+ const handleViewAll = useCallback(() => {
+ navigateToLatestEntitiesSearch(navigate);
+ }, [navigate]);
+
+ if (isLoading) return null;
+
+ return (
+ <Paper
+ elevation={1}
+ sx={{
+ padding: 2,
+ borderRadius: 2,
+ minHeight: 340,
+ minWidth: 0,
+ width: "100%",
+ flex: 1,
+ boxSizing: "border-box",
+ transition: "box-shadow 0.3s ease",
+ "&:hover": { boxShadow: 4 }
+ }}
+ >
+ <Box sx={{ pb: 2, borderBottom: "1px solid",
borderColor: "divider" }}>
+ <Stack direction="row"
justifyContent="space-between" alignItems="center">
+ <Typography sx={{ fontSize: "1rem",
fontWeight: 600, color: "#1a1a1a" }}>
+ Latest Entities Created
+ </Typography>
+ <Link
+ component="button"
+ onClick={handleViewAll}
+ sx={{
+ fontSize: "0.875rem",
+ cursor: "pointer",
+ textDecoration: "none",
+ color: "primary.main"
+ }}
+ aria-label="View all entities"
+ >
+ View All
+ </Link>
+ </Stack>
+ </Box>
+ {error ? (
+ <Stack alignItems="center"
justifyContent="center" height={200} sx={{ pt: 2 }}>
+ <Typography variant="body2"
color="error">
+ {error}
+ </Typography>
+ </Stack>
+ ) : !entities || entities.length === 0 ? (
+ <Stack alignItems="center"
justifyContent="center" height={200} sx={{ pt: 2 }}>
+ <Typography variant="body2"
color="text.secondary">
+ No recent entities
+ </Typography>
+ </Stack>
+ ) : (
+ <List disablePadding sx={{ pt: 2 }}>
+ {entities.slice(0, 7).map((entity) => {
+ const displayName =
resolveLatestEntityDisplayName(entity);
+ const entityGuid =
resolveLatestEntityGuid(entity);
+ const typeName =
resolveLatestEntityTypeName(entity);
+ const timestamp =
getEntityTimestampRawForDisplay(entity);
+ const detailHref = entityGuid
+ ?
`/detailPage/${entityGuid}`
+ : undefined;
+
+ return (
+ <ListItem
+ key={entityGuid
|| displayName}
+ disablePadding
+ sx={{
+ py: 1,
+
borderBottom: "1px solid",
+
borderColor: "divider",
+
"&:last-child": { borderBottom: "none" }
+ }}
+ >
+ <Stack
width="100%" direction="row" justifyContent="space-between" alignItems="center">
+ <Stack
direction="row" spacing={0.5} alignItems="center" flexWrap="wrap" flex={1}
minWidth={0} mr={1}>
+
{detailHref ? (
+
<Link
+
component={RouterLink}
+
to={detailHref}
+
underline="hover"
+
color="primary"
+
sx={{
+
fontSize: "0.875rem",
+
overflow: "hidden",
+
textOverflow: "ellipsis",
+
cursor: "pointer",
+
maxWidth: "100%"
+
}}
+
>
+
{displayName}
+
</Link>
+
) : (
+
<Typography
+
component="span"
+
sx={{
+
fontSize: "0.875rem",
+
fontWeight: 500,
+
color: "text.primary"
+
}}
+
>
+
{displayName}
+
</Typography>
+
)}
+
<Typography
+
component="span"
+
sx={{
+
fontSize: "0.875rem",
+
color: "#6c757d",
+
flexShrink: 0
+
}}
+
>
+
({typeName})
+
</Typography>
+ </Stack>
+
<Typography sx={{ fontSize: "0.8125rem", color: "#6c757d", flexShrink: 0, ml: 1
}}>
+
{formatRelativeTime(timestamp)}
+
</Typography>
+ </Stack>
+ </ListItem>
+ );
+ })}
+ </List>
+ )}
+ </Paper>
+ );
+});
+
+LatestEntitiesList.displayName = "LatestEntitiesList";
+
+export default LatestEntitiesList;
diff --git a/dashboard/src/views/DashboardOverview/LatestEntitiesSkeleton.tsx
b/dashboard/src/views/DashboardOverview/LatestEntitiesSkeleton.tsx
new file mode 100644
index 000000000..75e4f5960
--- /dev/null
+++ b/dashboard/src/views/DashboardOverview/LatestEntitiesSkeleton.tsx
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Paper, Stack, Box } from "@mui/material";
+import SkeletonLoader from "@components/SkeletonLoader";
+
+const LatestEntitiesSkeleton = () => (
+ <Paper
+ elevation={1}
+ sx={{
+ padding: 2,
+ borderRadius: 2,
+ minHeight: 340,
+ minWidth: 0,
+ width: "100%",
+ flex: 1,
+ boxSizing: "border-box",
+ transition: "box-shadow 0.3s ease",
+ "&:hover": { boxShadow: 4 }
+ }}
+ >
+ <Box sx={{ pb: 2, borderBottom: "1px solid", borderColor:
"divider" }}>
+ <Stack direction="row" justifyContent="space-between"
alignItems="center">
+ <SkeletonLoader animation="wave" variant="text"
count={1} width="60%" height={24} />
+ <SkeletonLoader animation="wave" variant="text"
count={1} width={60} height={20} />
+ </Stack>
+ </Box>
+ <Stack spacing={1} sx={{ pt: 2 }}>
+ {[1, 2, 3, 4, 5, 6, 7].map((i) => (
+ <Stack key={i} direction="row"
justifyContent="space-between" alignItems="center" sx={{ py: 0.5 }}>
+ <SkeletonLoader animation="wave"
variant="text" count={1} width={`${70 + (i % 3) * 10}%`} height={20} />
+ <SkeletonLoader animation="wave"
variant="text" count={1} width={80} height={18} />
+ </Stack>
+ ))}
+ </Stack>
+ </Paper>
+);
+
+export default LatestEntitiesSkeleton;
diff --git a/dashboard/src/views/DashboardOverview/latestEntitiesList.utils.ts
b/dashboard/src/views/DashboardOverview/latestEntitiesList.utils.ts
new file mode 100644
index 000000000..118be6b15
--- /dev/null
+++ b/dashboard/src/views/DashboardOverview/latestEntitiesList.utils.ts
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Shape of basic-search entity headers used by Latest Entities card. */
+export interface LatestEntityRowModel {
+ guid?: string;
+ name?: string;
+ typeName?: string;
+ displayText?: string;
+ attributes?: {
+ name?: string;
+ qualifiedName?: string;
+ __guid?: string;
+ };
+}
+
+/**
+ * Match Search name column: top-level name/displayText and attributes.name /
+ * qualifiedName (Atlas may return any of these).
+ */
+export const resolveLatestEntityDisplayName = (
+ entity: LatestEntityRowModel
+): string => {
+ const n =
+ entity.name ??
+ entity.attributes?.name ??
+ entity.attributes?.qualifiedName ??
+ entity.displayText ??
+ entity.guid;
+ return typeof n === "string" && n.trim() !== "" ? n.trim() : "Unknown";
+};
+
+export const resolveLatestEntityGuid = (
+ entity: LatestEntityRowModel
+): string | undefined => {
+ const g =
+ entity.guid ??
+ (typeof entity.attributes?.__guid === "string"
+ ? entity.attributes.__guid
+ : undefined);
+ if (typeof g !== "string" || g.trim() === "") return undefined;
+ return g.trim();
+};
+
+export const resolveLatestEntityTypeName = (
+ entity: LatestEntityRowModel
+): string => {
+ const t = entity.typeName;
+ return typeof t === "string" && t.trim() !== "" ? t.trim() : "Entity";
+};
diff --git a/dashboard/src/views/SearchResult/SearchResult.tsx
b/dashboard/src/views/SearchResult/SearchResult.tsx
index a026e1175..5d850d93a 100644
--- a/dashboard/src/views/SearchResult/SearchResult.tsx
+++ b/dashboard/src/views/SearchResult/SearchResult.tsx
@@ -71,6 +71,9 @@ interface Params {
classification: any;
termName: string | null;
relationshipFilters?: string | null;
+ sortBy?: string;
+ sortOrder?: string;
+ excludeHeaderAttributes?: boolean;
}
let defaultColumnsName: Array<string> = [
@@ -155,6 +158,15 @@ const SearchResult = ({ classificationParams,
glossaryTypeParams, hideFilters }:
? !searchParams.get("excludeST")
: true,
includeClassificationAttributes: true,
+ ...(!isEmpty(searchParams.get("sortBy")) && {
+ sortBy: searchParams.get("sortBy")
+ }),
+ ...(!isEmpty(searchParams.get("sortOrder")) && {
+ sortOrder: searchParams.get("sortOrder")
+ }),
+ ...(searchParams.get("excludeHeaderAttributes") === "true" && {
+ excludeHeaderAttributes: true
+ }),
...(isEmpty(classificationParams || glossaryTypeParams) && {
entityFilters: !isEmpty(entityFilterParams)
? searchParamsAPiQuery(entityFilterParams)
@@ -1000,12 +1012,20 @@ const SearchResult = ({ classificationParams,
glossaryTypeParams, hideFilters }:
return hideColumns;
};
+ const latestEntitiesSortBy = searchParams.get("sortBy");
+ const latestEntitiesSortOrder = searchParams.get("sortOrder");
const getDefaultSort = useMemo(() => {
if (isDslAggregate) {
return [] as any[]; // no default sorting for DSL aggregates
}
- return [{ id: "name", asc: true }];
- }, [isDslAggregate]);
+ if (
+ latestEntitiesSortBy === "__timestamp" &&
+ latestEntitiesSortOrder === "DESCENDING"
+ ) {
+ return [{ id: "__timestamp", desc: true }];
+ }
+ return [{ id: "name", desc: false }];
+ }, [isDslAggregate, latestEntitiesSortBy, latestEntitiesSortOrder]);
return (
<Stack position="relative" gap={"1rem"}>