diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 6cd2edb23..a317b41d3 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,5 +1,3 @@
-///
-
import { StoryFn } from "@storybook/react";
import React from "react";
import {
@@ -7,17 +5,19 @@ import {
initializeDigmaMessageListener,
sendMessage
} from "../src/api";
-import { App } from "../src/components/common/App";
+import { App, THEMES } from "../src/components/common/App";
import { dispatcher } from "../src/dispatcher";
+import { Mode } from "../src/globals";
export const decorators = [
- (Story: StoryFn): JSX.Element => {
+ (Story: StoryFn, context: { globals: { theme: Mode } }): JSX.Element => {
+ const theme = context.globals.theme;
initializeDigmaMessageListener(dispatcher);
window.sendMessageToDigma = sendMessage;
window.cancelMessageToDigma = cancelMessage;
return (
-
+
);
@@ -33,3 +33,14 @@ export const parameters = {
}
}
};
+
+export const globalTypes = {
+ theme: {
+ name: "Theme",
+ description: "Theme",
+ toolbar: {
+ title: "Theme",
+ items: THEMES
+ }
+ }
+};
diff --git a/package-lock.json b/package-lock.json
index fe0c4f988..b2838ca2a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
+ "@floating-ui/react": "^0.21.0",
"@tanstack/react-table": "^8.7.8",
"date-fns": "^2.29.3",
"react": "^18.2.0",
@@ -2290,6 +2291,45 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.3.tgz",
+ "integrity": "sha512-upVRtrNZuYNsw+EoxkiBFRPROnU8UTy/u/dZ9U0W14BlemPYODwhhxYXSR2Y9xOnvr1XtptJRWx7gL8Te1qaog=="
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.4.tgz",
+ "integrity": "sha512-4+k+BLhtWj+peCU60gp0+rHeR8+Ohqx6kjJf/lHMnJ8JD5Qj6jytcq1+SZzRwD7rvHKRhR7TDiWWddrNrfwQLg==",
+ "dependencies": {
+ "@floating-ui/core": "^1.2.3"
+ }
+ },
+ "node_modules/@floating-ui/react": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.21.0.tgz",
+ "integrity": "sha512-4Zut7tjeDVEKHaR6N3uG4m1dl114UkLuK4SNAeHlAb4pKu5KEkMkI34Y8NmCc4ARfXIu25UGUhYBUzShDhbofA==",
+ "dependencies": {
+ "@floating-ui/react-dom": "^1.3.0",
+ "aria-hidden": "^1.1.3",
+ "tabbable": "^6.0.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.3.0.tgz",
+ "integrity": "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==",
+ "dependencies": {
+ "@floating-ui/dom": "^1.2.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -10251,6 +10291,17 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
+ "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
@@ -23650,6 +23701,11 @@
"integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==",
"dev": true
},
+ "node_modules/tabbable": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.1.1.tgz",
+ "integrity": "sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg=="
+ },
"node_modules/table": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
@@ -24102,8 +24158,7 @@
"node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
- "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
- "dev": true
+ "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/tsutils": {
"version": "3.21.0",
@@ -27481,6 +27536,37 @@
"strip-json-comments": "^3.1.1"
}
},
+ "@floating-ui/core": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.3.tgz",
+ "integrity": "sha512-upVRtrNZuYNsw+EoxkiBFRPROnU8UTy/u/dZ9U0W14BlemPYODwhhxYXSR2Y9xOnvr1XtptJRWx7gL8Te1qaog=="
+ },
+ "@floating-ui/dom": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.4.tgz",
+ "integrity": "sha512-4+k+BLhtWj+peCU60gp0+rHeR8+Ohqx6kjJf/lHMnJ8JD5Qj6jytcq1+SZzRwD7rvHKRhR7TDiWWddrNrfwQLg==",
+ "requires": {
+ "@floating-ui/core": "^1.2.3"
+ }
+ },
+ "@floating-ui/react": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.21.0.tgz",
+ "integrity": "sha512-4Zut7tjeDVEKHaR6N3uG4m1dl114UkLuK4SNAeHlAb4pKu5KEkMkI34Y8NmCc4ARfXIu25UGUhYBUzShDhbofA==",
+ "requires": {
+ "@floating-ui/react-dom": "^1.3.0",
+ "aria-hidden": "^1.1.3",
+ "tabbable": "^6.0.1"
+ }
+ },
+ "@floating-ui/react-dom": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.3.0.tgz",
+ "integrity": "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==",
+ "requires": {
+ "@floating-ui/dom": "^1.2.1"
+ }
+ },
"@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -33685,6 +33771,14 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "aria-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
+ "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
"aria-query": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
@@ -44102,6 +44196,11 @@
"integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==",
"dev": true
},
+ "tabbable": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.1.1.tgz",
+ "integrity": "sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg=="
+ },
"table": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
@@ -44442,8 +44541,7 @@
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
- "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
- "dev": true
+ "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"tsutils": {
"version": "3.21.0",
diff --git a/package.json b/package.json
index 45adbb39d..6ff043b89 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
"webpack-merge": "^5.8.0"
},
"dependencies": {
+ "@floating-ui/react": "^0.21.0",
"@tanstack/react-table": "^8.7.8",
"date-fns": "^2.29.3",
"react": "^18.2.0",
diff --git a/src/components/Assets/AssetList/index.tsx b/src/components/Assets/AssetList/index.tsx
index 743c97c63..0711526a9 100644
--- a/src/components/Assets/AssetList/index.tsx
+++ b/src/components/Assets/AssetList/index.tsx
@@ -1,23 +1,142 @@
+import { useMemo, useState } from "react";
import { AssetEntry as AssetEntryComponent } from "../../common/AssetEntry";
import { ChevronIcon } from "../../common/icons/ChevronIcon";
import { DIRECTION } from "../../common/icons/types";
-import { AssetEntry } from "../types";
+import { Menu } from "../../common/Menu";
+import { Popover } from "../../common/Popover";
+import { PopoverContent } from "../../common/Popover/PopoverContent";
+import { PopoverTrigger } from "../../common/Popover/PopoverTrigger";
import { getAssetTypeInfo } from "../utils";
import * as s from "./styles";
-import { AssetListProps } from "./types";
+import {
+ AssetListProps,
+ ExtendedAssetEntryWithServices,
+ Sorting
+} from "./types";
+
+const SORTING_CRITERION = ["Critical insights", "Performance", "Name"];
+
+const sortEntries = (
+ entries: ExtendedAssetEntryWithServices[],
+ sorting: Sorting
+): ExtendedAssetEntryWithServices[] => {
+ entries = [...entries];
+
+ const sortByName = (
+ a: ExtendedAssetEntryWithServices,
+ b: ExtendedAssetEntryWithServices
+ ) => a.span.displayName.localeCompare(b.span.displayName);
+
+ switch (sorting.criterion) {
+ case "Critical insights":
+ return entries.sort((a, b) => {
+ const aCriticalInsights = a.insights.filter(
+ (x) => x.importance < 3
+ ).length;
+ const bCriticalInsights = b.insights.filter(
+ (x) => x.importance < 3
+ ).length;
+
+ return (
+ (sorting.isDesc
+ ? aCriticalInsights - bCriticalInsights
+ : bCriticalInsights - aCriticalInsights) || sortByName(a, b)
+ );
+ });
+ case "Performance":
+ return entries.sort((a, b) => {
+ const aDuration = a.durationPercentiles.find(
+ (duration) => duration.percentile === 0.5
+ )?.currentDuration.raw;
+ const bDuration = b.durationPercentiles.find(
+ (duration) => duration.percentile === 0.5
+ )?.currentDuration.raw;
+
+ if (!aDuration && !bDuration) {
+ return 0;
+ }
+
+ if (!aDuration) {
+ return sorting.isDesc ? -1 : 1;
+ }
+
+ if (!bDuration) {
+ return sorting.isDesc ? -1 : 1;
+ }
+
+ return (
+ (sorting.isDesc ? bDuration - aDuration : aDuration - bDuration) ||
+ sortByName(a, b)
+ );
+ });
+ case "Name":
+ return entries.sort(sortByName);
+ default:
+ return entries;
+ }
+};
export const AssetList = (props: AssetListProps) => {
+ const [sorting, setSorting] = useState<{
+ criterion: string;
+ isDesc: boolean;
+ }>({
+ criterion: "Critical insights",
+ isDesc: true
+ });
+ const [isSortingMenuOpen, setIsSortingMenuOpen] = useState(false);
+
const handleBackButtonClick = () => {
props.onBackButtonClick();
};
- const handleAssetLinkClick = (entry: AssetEntry) => {
+ const handleAssetLinkClick = (entry: ExtendedAssetEntryWithServices) => {
props.onAssetLinkClick(entry);
};
+ const handleSortingMenuToggle = () => {
+ setIsSortingMenuOpen(!isSortingMenuOpen);
+ };
+
+ const handleSortingMenuItemSelect = (value: string) => {
+ if (sorting.criterion === value) {
+ setSorting({
+ ...sorting,
+ isDesc: !sorting.isDesc
+ });
+ } else {
+ setSorting({
+ criterion: value,
+ isDesc: false
+ });
+ }
+ handleSortingMenuToggle();
+ };
+
const assetTypeInfo = getAssetTypeInfo(props.assetTypeId);
- const uniqueEntryIds = Object.keys(props.entries);
+ const entries: ExtendedAssetEntryWithServices[] = useMemo(
+ () =>
+ Object.keys(props.entries)
+ .map((entryId) => {
+ const entries = props.entries[entryId];
+ return entries.map((entry) => {
+ const relatedServices = entries.map((entry) => entry.serviceName);
+ return {
+ ...entry,
+ id: entryId,
+ relatedServices
+ };
+ });
+ })
+ .flat(),
+ [props.entries]
+ );
+
+ const sortedEntries = useMemo(
+ () => sortEntries(entries, sorting),
+ [entries, sorting]
+ );
return (
@@ -31,22 +150,41 @@ export const AssetList = (props: AssetListProps) => {
{Object.values(props.entries).flat().length}
- {uniqueEntryIds.length > 0 ? (
+
+
+
+
+ Sort by
+ {sorting.criterion}
+
+
+
+
+
+
+
+ {sortedEntries.length > 0 ? (
- {uniqueEntryIds.map((entryId) => {
- const entries = props.entries[entryId];
- return entries.map((entry) => {
- const services = entries.map((entry) => entry.serviceName);
-
- return (
-
- );
- });
+ {sortedEntries.map((entry) => {
+ return (
+
+ );
})}
) : (
diff --git a/src/components/Assets/AssetList/styles.ts b/src/components/Assets/AssetList/styles.ts
index ec1d9091f..565fbd327 100644
--- a/src/components/Assets/AssetList/styles.ts
+++ b/src/components/Assets/AssetList/styles.ts
@@ -25,14 +25,38 @@ export const Header = styled.div`
padding: 8px 12px 8px 8px;
`;
+export const Toolbar = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ padding: 8px;
+ gap: 12px;
+`;
+
+export const SortingMenuContainer = styled.div`
+ display: flex;
+ gap: 2px;
+ font-weight: 500;
+ font-size: 10px;
+ line-height: 12px;
+ color: #9b9b9b;
+ align-items: center;
+ height: 20px;
+`;
+
+export const SortingLabel = styled.span`
+ font-weight: 500;
+ font-size: 10px;
+ line-height: 12px;
+ color: #dadada;
+`;
+
export const ItemsCount = styled.span`
margin-left: auto;
- font-weight: 400;
color: #9f9f9f;
`;
export const List = styled.ul`
- padding: 8px 9px;
+ padding: 0 9px 8px;
display: flex;
flex-direction: column;
gap: 8px;
diff --git a/src/components/Assets/AssetList/types.ts b/src/components/Assets/AssetList/types.ts
index 83957346e..2606334f7 100644
--- a/src/components/Assets/AssetList/types.ts
+++ b/src/components/Assets/AssetList/types.ts
@@ -1,8 +1,17 @@
-import { AssetEntry } from "../types";
+import { ExtendedAssetEntry } from "../types";
+
+export interface ExtendedAssetEntryWithServices extends ExtendedAssetEntry {
+ relatedServices: string[];
+}
export interface AssetListProps {
onBackButtonClick: () => void;
assetTypeId: string;
- entries: { [key: string]: AssetEntry[] };
- onAssetLinkClick: (entry: AssetEntry) => void;
+ entries: { [key: string]: ExtendedAssetEntry[] };
+ onAssetLinkClick: (entry: ExtendedAssetEntryWithServices) => void;
+}
+
+export interface Sorting {
+ criterion: string;
+ isDesc: boolean;
}
diff --git a/src/components/Assets/AssetTypeList/types.ts b/src/components/Assets/AssetTypeList/types.ts
index 6c429b800..df5949294 100644
--- a/src/components/Assets/AssetTypeList/types.ts
+++ b/src/components/Assets/AssetTypeList/types.ts
@@ -1,6 +1,6 @@
-import { AssetEntry } from "../types";
+import { ExtendedAssetEntry } from "../types";
-export type AssetListProps = {
- data: { [key: string]: { [key: string]: AssetEntry[] } };
+export interface AssetListProps {
+ data: { [key: string]: { [key: string]: ExtendedAssetEntry[] } };
onAssetTypeSelect: (categoryId: string) => void;
-};
+}
diff --git a/src/components/RecentActivity/RecentActivityTable/types.ts b/src/components/RecentActivity/RecentActivityTable/types.ts
index 43f3310b4..5954bcbde 100644
--- a/src/components/RecentActivity/RecentActivityTable/types.ts
+++ b/src/components/RecentActivity/RecentActivityTable/types.ts
@@ -1,12 +1,12 @@
import { ViewMode } from "../EnvironmentPanel/types";
import { ActivityEntry, EntrySpan } from "../types";
-export type RecentActivityTableProps = {
+export interface RecentActivityTableProps {
data: ActivityEntry[];
onSpanLinkClick: (span: EntrySpan) => void;
onTraceButtonClick: (traceId: string, span: EntrySpan) => void;
viewMode: ViewMode;
-};
+}
export enum INSIGHT_TYPES {
SpanUsageStatus = "SpanUsageStatus",
diff --git a/src/components/RecentActivity/types.ts b/src/components/RecentActivity/types.ts
index a28fa79c2..b1bac30ed 100644
--- a/src/components/RecentActivity/types.ts
+++ b/src/components/RecentActivity/types.ts
@@ -1,19 +1,19 @@
import { Duration } from "../../globals";
-export type EntrySpan = {
+export interface EntrySpan {
displayText: string;
serviceName: string;
scopeId: string;
spanCodeObjectId: string;
methodCodeObjectId: string | null;
-};
+}
-export type SlimInsight = {
+export interface SlimInsight {
type: string;
codeObjectIds: string[];
-};
+}
-export type ActivityEntry = {
+export interface ActivityEntry {
environment: string;
traceFlowDisplayName: string;
firstEntrySpan: EntrySpan;
@@ -22,7 +22,7 @@ export type ActivityEntry = {
latestTraceTimestamp: string;
latestTraceDuration: Duration;
slimAggregatedInsights: SlimInsight[];
-};
+}
export interface RecentActivityData {
environments: string[];
diff --git a/src/components/common/App/index.tsx b/src/components/common/App/index.tsx
index cae63d448..aa6842c43 100644
--- a/src/components/common/App/index.tsx
+++ b/src/components/common/App/index.tsx
@@ -7,11 +7,10 @@ import { isString } from "../../../typeGuards/isString";
import { GlobalStyle } from "./styles";
import { AppProps } from "./types";
+export const THEMES = ["light", "dark", "dark-jetbrains"];
+
const isMode = (mode: unknown): mode is Mode => {
- return (
- typeof mode === "string" &&
- ["light", "dark", "dark-jetbrains"].includes(mode)
- );
+ return typeof mode === "string" && THEMES.includes(mode);
};
const getMode = (): Mode => {
@@ -36,6 +35,13 @@ export const App = (props: AppProps) => {
const [mainFont, setMainFont] = useState("");
const [codeFont, setCodeFont] = useState("");
+ useEffect(() => {
+ if (!props.theme) {
+ return;
+ }
+ setMode(props.theme);
+ }, [props.theme]);
+
useEffect(() => {
const handleSetColorMode = (data: unknown) => {
if (isObject(data) && isMode(data.theme)) {
diff --git a/src/components/common/App/types.ts b/src/components/common/App/types.ts
index aecc635ce..d648581f9 100644
--- a/src/components/common/App/types.ts
+++ b/src/components/common/App/types.ts
@@ -1,3 +1,6 @@
+import { Mode } from "../../../globals";
+
export interface AppProps {
children: React.ReactNode;
+ theme?: Mode;
}
diff --git a/src/components/common/AssetEntry/AssetEntry.stories.tsx b/src/components/common/AssetEntry/AssetEntry.stories.tsx
index e11311ef3..46d157394 100644
--- a/src/components/common/AssetEntry/AssetEntry.stories.tsx
+++ b/src/components/common/AssetEntry/AssetEntry.stories.tsx
@@ -18,8 +18,9 @@ const Template: ComponentStory = (args: AssetEntryProps) => (
export const Default = Template.bind({});
Default.args = {
- relatedServices: ["service1", "service2"],
entry: {
+ id: "span:io.opentelemetry.tomcat-10.0$_$HTTP GET /SampleInsights/ErrorRecordedOnLocalRootSpan",
+ relatedServices: ["service1", "service2"],
span: {
classification: "Endpoint",
role: "Entry",
diff --git a/src/components/common/AssetEntry/index.tsx b/src/components/common/AssetEntry/index.tsx
index f5fa1ff0f..9ea68ecbd 100644
--- a/src/components/common/AssetEntry/index.tsx
+++ b/src/components/common/AssetEntry/index.tsx
@@ -13,12 +13,13 @@ export const AssetEntry = (props: AssetEntryProps) => {
};
const name = props.entry.span.displayName;
- const otherServices = props.relatedServices.filter(
+ const otherServices = props.entry.relatedServices.filter(
(service) => service !== props.entry.serviceName
);
const performanceDuration = props.entry.durationPercentiles.find(
(duration) => duration.percentile === 0.5
)?.currentDuration;
+
const lastSeenDateTime = props.entry.lastSpanInstanceInfo.startTime;
return (
diff --git a/src/components/common/AssetEntry/types.ts b/src/components/common/AssetEntry/types.ts
index e0f560c0d..b64b9a849 100644
--- a/src/components/common/AssetEntry/types.ts
+++ b/src/components/common/AssetEntry/types.ts
@@ -1,7 +1,6 @@
-import { AssetEntry } from "../../Assets/types";
+import { ExtendedAssetEntryWithServices } from "../../Assets/AssetList/types";
export interface AssetEntryProps {
- entry: AssetEntry;
- relatedServices: string[];
- onAssetLinkClick: (entry: AssetEntry) => void;
+ entry: ExtendedAssetEntryWithServices;
+ onAssetLinkClick: (entry: ExtendedAssetEntryWithServices) => void;
}
diff --git a/src/components/common/Menu/Menu.stories.tsx b/src/components/common/Menu/Menu.stories.tsx
new file mode 100644
index 000000000..2ecbe2473
--- /dev/null
+++ b/src/components/common/Menu/Menu.stories.tsx
@@ -0,0 +1,36 @@
+import { ComponentMeta, ComponentStory } from "@storybook/react";
+
+import { Menu } from ".";
+import { MenuProps } from "./types";
+
+export default {
+ title: "Common/Menu",
+ component: Menu,
+ parameters: {
+ // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
+ layout: "fullscreen"
+ }
+} as ComponentMeta;
+
+const Template: ComponentStory = (args: MenuProps) => (
+
+);
+
+export const Default = Template.bind({});
+Default.args = {
+ title: "Title",
+ items: [
+ {
+ label: "Item 1",
+ value: "item_1"
+ },
+ {
+ label: "Item 2",
+ value: "item_2"
+ },
+ {
+ label: "Item 3",
+ value: "item_3"
+ }
+ ]
+};
diff --git a/src/components/common/Menu/index.tsx b/src/components/common/Menu/index.tsx
new file mode 100644
index 000000000..83fc5360a
--- /dev/null
+++ b/src/components/common/Menu/index.tsx
@@ -0,0 +1,24 @@
+import * as s from "./styles";
+import { MenuProps } from "./types";
+
+export const Menu = (props: MenuProps) => {
+ const handleMenuItemClick = (value: string) => {
+ props.onSelect(value);
+ };
+
+ return (
+
+ {props.title}
+
+ {props.items.map((item) => (
+ handleMenuItemClick(item.value)}
+ >
+ {item.label}
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/common/Menu/styles.ts b/src/components/common/Menu/styles.ts
new file mode 100644
index 000000000..ec10931fd
--- /dev/null
+++ b/src/components/common/Menu/styles.ts
@@ -0,0 +1,34 @@
+import styled from "styled-components";
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ background: #2e2e2e;
+ box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.12);
+ border-radius: 2px;
+`;
+
+export const Header = styled.div`
+ padding: 2px 8px;
+ font-size: 10px;
+ line-height: 14px;
+ color: #7c7c94;
+`;
+
+export const List = styled.ul`
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0;
+`;
+
+export const ListItem = styled.li`
+ flex-direction: row;
+ width: 100%;
+ list-style-type: none;
+ padding: 6px 8px;
+ font-size: 10px;
+ line-height: 12px;
+ color: #9b9b9b;
+ cursor: pointer;
+`;
diff --git a/src/components/common/Menu/types.ts b/src/components/common/Menu/types.ts
new file mode 100644
index 000000000..bfa1095d3
--- /dev/null
+++ b/src/components/common/Menu/types.ts
@@ -0,0 +1,8 @@
+export interface MenuProps {
+ title: string;
+ items: {
+ label: string;
+ value: string;
+ }[];
+ onSelect: (value: string) => void;
+}
diff --git a/src/components/common/Popover/PopoverContent/index.tsx b/src/components/common/Popover/PopoverContent/index.tsx
new file mode 100644
index 000000000..0e56bb390
--- /dev/null
+++ b/src/components/common/Popover/PopoverContent/index.tsx
@@ -0,0 +1,48 @@
+// Source: https://floating-ui.com/docs/popover#reusable-popover-component
+
+import {
+ FloatingFocusManager,
+ FloatingPortal,
+ useMergeRefs
+} from "@floating-ui/react";
+import { CSSProperties, ForwardedRef, forwardRef, HTMLProps } from "react";
+import { usePopoverContext } from "../hooks";
+
+const PopoverContentComponent = (
+ props: HTMLProps,
+ propRef: ForwardedRef
+) => {
+ const { context: floatingContext, ...context } = usePopoverContext();
+ const ref = useMergeRefs([context.refs.setFloating, propRef]);
+
+ return (
+
+ {context.open && (
+
+
+ {props.children}
+
+
+ )}
+
+ );
+};
+
+export const PopoverContent = forwardRef<
+ HTMLDivElement,
+ HTMLProps
+>(PopoverContentComponent);
diff --git a/src/components/common/Popover/PopoverContent/types.ts b/src/components/common/Popover/PopoverContent/types.ts
new file mode 100644
index 000000000..c06cb2981
--- /dev/null
+++ b/src/components/common/Popover/PopoverContent/types.ts
@@ -0,0 +1,6 @@
+import { CSSProperties, ReactNode } from "react";
+
+export interface PopoverContentProps {
+ children: ReactNode;
+ style: CSSProperties;
+}
diff --git a/src/components/common/Popover/PopoverTrigger/index.tsx b/src/components/common/Popover/PopoverTrigger/index.tsx
new file mode 100644
index 000000000..2f9b4b576
--- /dev/null
+++ b/src/components/common/Popover/PopoverTrigger/index.tsx
@@ -0,0 +1,61 @@
+// Source: https://floating-ui.com/docs/popover#reusable-popover-component
+
+import { useMergeRefs } from "@floating-ui/react";
+import {
+ cloneElement,
+ ForwardedRef,
+ forwardRef,
+ HTMLProps,
+ isValidElement,
+ Ref
+} from "react";
+import { usePopoverContext } from "../hooks";
+import * as s from "./styles";
+import { PopoverTriggerProps } from "./types";
+
+const PopoverTriggerComponent = (
+ {
+ children,
+ asChild = false,
+ ...props
+ }: HTMLProps & PopoverTriggerProps,
+ propRef: ForwardedRef
+) => {
+ const context = usePopoverContext();
+ // TODO: improve types
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
+ const childrenRef: Ref = (children as any).ref;
+ const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);
+
+ // `asChild` allows the user to pass any element as the anchor
+ if (asChild && isValidElement(children)) {
+ return cloneElement(
+ children,
+ // TODO: improve types
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
+ context.getReferenceProps({
+ ref,
+ ...props,
+ ...children.props,
+ "data-state": context.open ? "open" : "closed"
+ })
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const PopoverTrigger = forwardRef<
+ HTMLElement,
+ HTMLProps & PopoverTriggerProps
+>(PopoverTriggerComponent);
diff --git a/src/components/common/Popover/PopoverTrigger/styles.ts b/src/components/common/Popover/PopoverTrigger/styles.ts
new file mode 100644
index 000000000..f228829d5
--- /dev/null
+++ b/src/components/common/Popover/PopoverTrigger/styles.ts
@@ -0,0 +1,9 @@
+import styled from "styled-components";
+
+export const Button = styled.button`
+ background: none;
+ border: none;
+ display: flex;
+ padding: 0;
+ cursor: pointer;
+`;
diff --git a/src/components/common/Popover/PopoverTrigger/types.ts b/src/components/common/Popover/PopoverTrigger/types.ts
new file mode 100644
index 000000000..e8fb05493
--- /dev/null
+++ b/src/components/common/Popover/PopoverTrigger/types.ts
@@ -0,0 +1,4 @@
+export interface PopoverTriggerProps {
+ children: React.ReactNode;
+ asChild?: boolean;
+}
diff --git a/src/components/common/Popover/hooks.ts b/src/components/common/Popover/hooks.ts
new file mode 100644
index 000000000..bc286a58c
--- /dev/null
+++ b/src/components/common/Popover/hooks.ts
@@ -0,0 +1,81 @@
+// Source: https://floating-ui.com/docs/popover#reusable-popover-component
+
+import {
+ autoUpdate,
+ flip,
+ offset,
+ shift,
+ useClick,
+ useDismiss,
+ useFloating,
+ useInteractions,
+ useRole
+} from "@floating-ui/react";
+import { createContext, useContext, useMemo, useState } from "react";
+import { ContextType, PopoverProps } from "./types";
+
+export const usePopover = ({
+ initialOpen = false,
+ placement = "bottom",
+ modal,
+ open: controlledOpen,
+ onOpenChange: setControlledOpen
+}: PopoverProps = {}) => {
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
+ const [labelId, setLabelId] = useState();
+ const [descriptionId, setDescriptionId] = useState();
+
+ const open = controlledOpen ?? uncontrolledOpen;
+ const setOpen = setControlledOpen ?? setUncontrolledOpen;
+
+ const data = useFloating({
+ placement,
+ open,
+ onOpenChange: setOpen,
+ whileElementsMounted: autoUpdate,
+ middleware: [
+ offset(5),
+ flip({
+ fallbackAxisSideDirection: "end"
+ }),
+ shift({ padding: 5 })
+ ]
+ });
+
+ const context = data.context;
+
+ const click = useClick(context, {
+ enabled: controlledOpen == null
+ });
+ const dismiss = useDismiss(context);
+ const role = useRole(context);
+
+ const interactions = useInteractions([click, dismiss, role]);
+
+ return useMemo(
+ () => ({
+ open,
+ setOpen,
+ ...interactions,
+ ...data,
+ modal,
+ labelId,
+ descriptionId,
+ setLabelId,
+ setDescriptionId
+ }),
+ [open, setOpen, interactions, data, modal, labelId, descriptionId]
+ );
+};
+
+export const PopoverContext = createContext(null);
+
+export const usePopoverContext = () => {
+ const context = useContext(PopoverContext);
+
+ if (context == null) {
+ throw new Error("Popover components must be wrapped in ");
+ }
+
+ return context;
+};
diff --git a/src/components/common/Popover/index.tsx b/src/components/common/Popover/index.tsx
new file mode 100644
index 000000000..9fb94a750
--- /dev/null
+++ b/src/components/common/Popover/index.tsx
@@ -0,0 +1,21 @@
+// Source: https://floating-ui.com/docs/popover#reusable-popover-component
+
+import { PopoverContext, usePopover } from "./hooks";
+import { PopoverProps } from "./types";
+
+export function Popover({
+ children,
+ modal = false,
+ ...restOptions
+}: {
+ children: React.ReactNode;
+} & PopoverProps) {
+ // This can accept any props as options, e.g. `placement`,
+ // or other positioning options.
+ const popover = usePopover({ modal, ...restOptions });
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/common/Popover/types.ts b/src/components/common/Popover/types.ts
new file mode 100644
index 000000000..a50d019f0
--- /dev/null
+++ b/src/components/common/Popover/types.ts
@@ -0,0 +1,19 @@
+import { Placement } from "@floating-ui/react";
+import { usePopover } from "./hooks";
+
+export type ContextType =
+ | (ReturnType & {
+ setLabelId: React.Dispatch>;
+ setDescriptionId: React.Dispatch<
+ React.SetStateAction
+ >;
+ })
+ | null;
+
+export interface PopoverProps {
+ initialOpen?: boolean;
+ placement?: Placement;
+ modal?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+}
diff --git a/src/environment.ts b/src/environment.ts
index 561d8e02a..376fc109b 100644
--- a/src/environment.ts
+++ b/src/environment.ts
@@ -1,9 +1,11 @@
import { Environment } from "./globals";
+const ENVIRONMENTS = ["JetBrains", "VS Code", "Other"];
+
+const isEnvironment = (environment: unknown): environment is Environment =>
+ typeof environment === "string" && ENVIRONMENTS.includes(environment);
+
export const getEnvironment = (): Environment =>
- typeof window.environment === "string" &&
- ["JetBrains", "VS Code", "Other"].includes(window.environment)
- ? window.environment
- : "Other";
+ isEnvironment(window.environment) ? window.environment : "Other";
export const environment = getEnvironment();
diff --git a/src/globals.d.ts b/src/globals.d.ts
index 6bbff55bf..611270b72 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -6,29 +6,29 @@ export type Mode = "light" | "dark" | "dark-jetbrains";
declare global {
interface Window {
- sendMessageToVSCode: (message) => void;
- cefQuery: (query: {
+ sendMessageToVSCode?: (message) => void;
+ cefQuery?: (query: {
request: string;
persistent?: boolean;
onSuccess: (response) => void;
onFailure: (error_code, error_message) => void;
}) => string;
- cefQueryCancel: (request_id: string) => void;
+ cefQueryCancel?: (request_id: string) => void;
sendMessageToDigma: (message: any) => string | undefined;
cancelMessageToDigma: (request_id: string) => void;
- theme?: Mode;
- environment?: Environment;
- mainFont?: string;
- codeFont?: string;
- recentActivityRefreshInterval?: number;
- recentActivityExpirationLimit?: number;
- recentActivityDocumentationURL?: string;
- assetsRefreshInterval?: number;
+ theme?: unknown;
+ environment?: unknown;
+ mainFont?: unknown;
+ codeFont?: unknown;
+ recentActivityRefreshInterval?: unknown;
+ recentActivityExpirationLimit?: unknown;
+ recentActivityDocumentationURL?: unknown;
+ assetsRefreshInterval?: unknown;
}
}
-export type Duration = {
+export interface Duration {
value: number;
unit: string;
raw: number;
-};
+}