@@ -146,3 +155,9 @@ export function EditSourceForm() {
);
}
+
+const DrawerContainer = styled.div`
+ position: absolute;
+ right: 32px;
+ top: 16px;
+`;
diff --git a/frontend/webapp/containers/main/sources/source-describe/index.tsx b/frontend/webapp/containers/main/sources/source-describe/index.tsx
new file mode 100644
index 000000000..580bc30d3
--- /dev/null
+++ b/frontend/webapp/containers/main/sources/source-describe/index.tsx
@@ -0,0 +1,282 @@
+'use client';
+import React, { useEffect, useState } from 'react';
+import { Describe, Refresh } from '@/assets';
+import theme from '@/styles/palette';
+import { useDescribe } from '@/hooks';
+import styled from 'styled-components';
+import { Drawer, KeyvalText } from '@/design.system';
+
+interface SourceDescriptionDrawerProps {
+ namespace: string;
+ kind: string;
+ name: string;
+}
+
+interface DescribeItem {
+ name: string;
+ value: string;
+ explain?: string;
+}
+
+export const SourceDescriptionDrawer: React.FC<
+ SourceDescriptionDrawerProps
+> = ({ namespace, kind, name }) => {
+ const [isOpen, setDrawerOpen] = useState(false);
+ const [showExplanation, setShowExplanation] = useState<{
+ [key: string]: boolean;
+ }>({});
+ const [badgeStatus, setBadgeStatus] = useState<
+ 'error' | 'transitioning' | 'success'
+ >('success');
+
+ const toggleDrawer = () => setDrawerOpen(!isOpen);
+
+ const {
+ sourceDescription,
+ isSourceLoading,
+ fetchSourceDescription,
+ setNamespaceKindName,
+ } = useDescribe();
+
+ useEffect(() => {
+ isOpen &&
+ namespace &&
+ kind &&
+ name &&
+ setNamespaceKindName(namespace, kind, name);
+ }, [isOpen, namespace, kind, name]);
+
+ useEffect(() => {
+ if (sourceDescription) {
+ const statuses = extractSourceStatuses(sourceDescription);
+ if (statuses.includes('error')) setBadgeStatus('error');
+ else if (statuses.includes('transitioning'))
+ setBadgeStatus('transitioning');
+ else setBadgeStatus('success');
+ }
+ }, [sourceDescription]);
+
+ const handleToggleExplanation = (key: string) => {
+ setShowExplanation((prev) => ({
+ ...prev,
+ [key]: !prev[key],
+ }));
+ };
+
+ return (
+ <>
+
+
+ {!isSourceLoading && (
+
+
+ {badgeStatus === 'transitioning'
+ ? '...'
+ : badgeStatus === 'error'
+ ? '!'
+ : ''}
+
+
+ )}
+
+
+ {isOpen && (
+
setDrawerOpen(false)}
+ position="right"
+ width="fit-content"
+ >
+ {isSourceLoading ? (
+ Loading source details...
+ ) : (
+
+ {sourceDescription
+ ? formatDescription(
+ sourceDescription,
+ fetchSourceDescription,
+ handleToggleExplanation,
+ showExplanation
+ )
+ : 'No source details available.'}
+
+ )}
+
+ )}
+ >
+ );
+};
+
+function extractSourceStatuses(description: any): string[] {
+ const statuses: string[] = [];
+ if (description.instrumentationConfig?.status) {
+ statuses.push(description.instrumentationConfig.status);
+ }
+ description.pods?.forEach((pod: any) => {
+ if (pod.phase.status) statuses.push(pod.phase.status);
+ });
+ return statuses;
+}
+
+// Generic function to format any description data
+function formatDescription(
+ description: any,
+ refetch: () => void,
+ handleToggleExplanation: (key: string) => void,
+ showExplanation: { [key: string]: boolean }
+) {
+ const renderObjectProperties = (obj: any, parentKey = '') => {
+ return Object.entries(obj).map(([key, item]: [string, DescribeItem]) => {
+ const uniqueKey = `${parentKey}.${key}.${JSON.stringify(item)}`;
+
+ if (
+ typeof item === 'object' &&
+ item !== null &&
+ item.hasOwnProperty('value') &&
+ item.hasOwnProperty('name')
+ ) {
+ return (
+
+
handleToggleExplanation(uniqueKey)}
+ style={{ cursor: 'pointer' }}
+ >
+ {item.name}: {String(item.value)}
+
+ {showExplanation[uniqueKey] && item.explain && (
+
{item.explain}
+ )}
+
+ );
+ } else if (typeof item === 'object' && item !== null) {
+ return (
+
+ {renderObjectProperties(item)}
+
+ );
+ } else if (Array.isArray(item)) {
+ return
;
+ }
+ return null;
+ });
+ };
+
+ return (
+
+
+ {description.name?.value || 'Unnamed'}
+
+
+
+
+ {renderObjectProperties(description)}
+
+ );
+}
+// Component to handle pod data display
+const CollectorSection: React.FC<{ title: string; collector: any[] }> = ({
+ title,
+ collector,
+}) => (
+
+ {title}
+ {collector.map((item: any, index: number) => (
+
+ ))}
+
+);
+
+// Component to handle individual pod items with conditional styling based on status
+const CollectorItem: React.FC<{
+ label: string;
+ value: any;
+ status?: string;
+}> = ({ label, value, status }) => {
+ const color = status === 'error' ? theme.colors.error : theme.text.light_grey;
+
+ return (
+
+ - {label}: {String(value)}
+
+ );
+};
+
+const VersionHeader = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 10px;
+`;
+
+const VersionText = styled(KeyvalText)`
+ font-size: 24px;
+`;
+
+const CollectorTitle = styled(KeyvalText)`
+ font-size: 20px;
+ margin-bottom: 10px;
+`;
+
+const NotificationBadge = styled.div<{ status: string }>`
+ position: absolute;
+ top: -4px;
+ right: -4px;
+ background-color: ${({ status }) =>
+ status === 'error'
+ ? theme.colors.error
+ : status === 'transitioning'
+ ? theme.colors.orange_brown
+ : theme.colors.success};
+ color: white;
+ border-radius: 50%;
+ width: 16px;
+ height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+`;
+
+const IconWrapper = styled.div`
+ position: relative;
+ padding: 8px;
+ width: 16px;
+ border-radius: 8px;
+ border: 1px solid ${theme.colors.blue_grey};
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ &:hover {
+ background-color: ${theme.colors.dark};
+ }
+`;
+
+const LoadingMessage = styled.p`
+ font-size: 1rem;
+ color: #555;
+`;
+
+const DescriptionContent = styled(KeyvalText)`
+ white-space: pre-wrap;
+ line-height: 1.6;
+ padding: 20px;
+`;
+
+const StatusText = styled.div<{ color: string }>`
+ color: ${({ color }) => color};
+ font-weight: bold;
+ margin-bottom: 8px;
+ padding-left: 16px;
+`;
+
+const ExplanationText = styled.p`
+ font-size: 0.9rem;
+ color: ${theme.text.light_grey};
+ margin-top: -5px;
+ margin-bottom: 10px;
+`;
diff --git a/frontend/webapp/hooks/describe/index.ts b/frontend/webapp/hooks/describe/index.ts
new file mode 100644
index 000000000..34deb4135
--- /dev/null
+++ b/frontend/webapp/hooks/describe/index.ts
@@ -0,0 +1 @@
+export * from './useDescribe';
diff --git a/frontend/webapp/hooks/describe/useDescribe.ts b/frontend/webapp/hooks/describe/useDescribe.ts
new file mode 100644
index 000000000..625982cbe
--- /dev/null
+++ b/frontend/webapp/hooks/describe/useDescribe.ts
@@ -0,0 +1,70 @@
+import { useEffect, useState } from 'react';
+import { useQuery } from 'react-query';
+import { getOdigosDescription, getSourceDescription } from '@/services';
+
+export function useDescribe() {
+ const [namespace, setNamespace] = useState
('');
+ const [kind, setKind] = useState('');
+ const [name, setName] = useState('');
+
+ // Fetch Odigos description
+ const {
+ data: odigosDescription,
+ isLoading: isOdigosLoading,
+ refetch: refetchOdigosDescription,
+ } = useQuery(['odigosDescription'], getOdigosDescription, {
+ enabled: false,
+ });
+
+ // Fetch source description based on namespace, kind, and name
+ const {
+ data: sourceDescription,
+ isLoading: isSourceLoading,
+ refetch: refetchSourceDescription,
+ } = useQuery(
+ ['sourceDescription'],
+ () => getSourceDescription(namespace, kind.toLowerCase(), name),
+
+ {
+ onError: (error) => {
+ console.log(error);
+ },
+ enabled: false,
+ }
+ );
+
+ useEffect(() => {
+ if (namespace && kind && name) {
+ refetchSourceDescription();
+ }
+ }, [namespace, kind, name]);
+
+ useEffect(() => {
+ console.log({ sourceDescription });
+ }, [sourceDescription]);
+
+ // Function to set parameters for source description and refetch
+ function fetchSourceDescription() {
+ refetchSourceDescription();
+ }
+
+ function setNamespaceKindName(
+ newNamespace: string,
+ newKind: string,
+ newName: string
+ ) {
+ setNamespace(newNamespace);
+ setKind(newKind);
+ setName(newName);
+ }
+
+ return {
+ odigosDescription,
+ sourceDescription,
+ isOdigosLoading,
+ isSourceLoading,
+ refetchOdigosDescription,
+ fetchSourceDescription,
+ setNamespaceKindName,
+ };
+}
diff --git a/frontend/webapp/hooks/index.tsx b/frontend/webapp/hooks/index.tsx
index 5a9bee729..9528788b7 100644
--- a/frontend/webapp/hooks/index.tsx
+++ b/frontend/webapp/hooks/index.tsx
@@ -9,3 +9,4 @@ export * from './useNotify';
export * from './useSSE';
export * from './useOverviewMetrics';
export * from './instrumentation-rules';
+export * from './describe';
diff --git a/frontend/webapp/services/api.ts b/frontend/webapp/services/api.ts
index e02ed4921..915fb68c0 100644
--- a/frontend/webapp/services/api.ts
+++ b/frontend/webapp/services/api.ts
@@ -1,10 +1,19 @@
import axios from 'axios';
-export async function get(url: string) {
- const { data, status } = await axios.get(url);
- if (status === 200) {
- return data;
+export async function get(url: string, headers: Record = {}) {
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ ...headers,
+ Accept: 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch data from ${url}`);
}
+
+ return response.json();
}
export async function post(url: string, body: any) {
diff --git a/frontend/webapp/services/describe.ts b/frontend/webapp/services/describe.ts
new file mode 100644
index 000000000..2a2975bde
--- /dev/null
+++ b/frontend/webapp/services/describe.ts
@@ -0,0 +1,16 @@
+import { get } from './api';
+import { API } from '@/utils';
+
+// Function to get Odigos description
+export async function getOdigosDescription(): Promise {
+ return get(API.DESCRIBE_ODIGOS);
+}
+
+// Function to get source description based on namespace, kind, and name
+export async function getSourceDescription(
+ namespace: string,
+ kind: string,
+ name: string
+): Promise {
+ return get(API.DESCRIBE_SOURCE(namespace, kind, name));
+}
diff --git a/frontend/webapp/services/index.ts b/frontend/webapp/services/index.ts
index 7ecd1a2f9..a6ef21f8b 100644
--- a/frontend/webapp/services/index.ts
+++ b/frontend/webapp/services/index.ts
@@ -3,3 +3,4 @@ export * from './sources';
export * from './config';
export * from './actions';
export * from './instrumentation-rules';
+export * from './describe';
diff --git a/frontend/webapp/utils/constants/urls.tsx b/frontend/webapp/utils/constants/urls.tsx
index 9fddce502..42ebd6e9e 100644
--- a/frontend/webapp/utils/constants/urls.tsx
+++ b/frontend/webapp/utils/constants/urls.tsx
@@ -22,6 +22,9 @@ const API = {
INSTRUMENTATION_RULES: `${BASE_URL}/instrumentation-rules`,
INSTRUMENTATION_RULE: (id: string) =>
`${BASE_URL}/instrumentation-rules/${id}`,
+ DESCRIBE_ODIGOS: `${BASE_URL}/describe/odigos`,
+ DESCRIBE_SOURCE: (namespace: string, kind: string, name: string) =>
+ `${BASE_URL}/describe/source/namespace/${namespace}/kind/${kind}/name/${name}`,
};
const QUERIES = {
diff --git a/tests/e2e/cli-upgrade/assert-instrumented-and-pipeline.yaml b/tests/e2e/cli-upgrade/assert-instrumented-and-pipeline.yaml
index eb08db461..e83ab4ced 100644
--- a/tests/e2e/cli-upgrade/assert-instrumented-and-pipeline.yaml
+++ b/tests/e2e/cli-upgrade/assert-instrumented-and-pipeline.yaml
@@ -22,7 +22,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
labels:
- odigos.io/collector-role: "CLUSTER_GATEWAY"
+ odigos.io/collector-role: 'CLUSTER_GATEWAY'
name: odigos-gateway
namespace: odigos-test-cli-upgrade
ownerReferences:
@@ -35,11 +35,11 @@ spec:
replicas: 1
selector:
matchLabels:
- odigos.io/collector-role: "CLUSTER_GATEWAY"
+ odigos.io/collector-role: 'CLUSTER_GATEWAY'
template:
metadata:
labels:
- odigos.io/collector-role: "CLUSTER_GATEWAY"
+ odigos.io/collector-role: 'CLUSTER_GATEWAY'
spec:
containers:
- env:
@@ -105,7 +105,7 @@ apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
- odigos.io/collector-role: "NODE_COLLECTOR"
+ odigos.io/collector-role: 'NODE_COLLECTOR'
name: odigos-data-collection
namespace: odigos-test-cli-upgrade
ownerReferences:
@@ -117,11 +117,11 @@ metadata:
spec:
selector:
matchLabels:
- odigos.io/collector-role: "NODE_COLLECTOR"
+ odigos.io/collector-role: 'NODE_COLLECTOR'
template:
metadata:
labels:
- odigos.io/collector-role: "NODE_COLLECTOR"
+ odigos.io/collector-role: 'NODE_COLLECTOR'
spec:
containers:
- name: data-collection
@@ -166,15 +166,15 @@ spec:
name: conf
- hostPath:
path: /var/log
- type: ""
+ type: ''
name: varlog
- hostPath:
path: /var/lib/docker/containers
- type: ""
+ type: ''
name: varlibdockercontainers
- hostPath:
path: /var/lib/kubelet/pod-resources
- type: ""
+ type: ''
name: kubeletpodresources
status:
numberAvailable: 1
@@ -191,9 +191,9 @@ spec:
- name: frontend
resources:
limits:
- instrumentation.odigos.io/java-native-community: "1"
+ instrumentation.odigos.io/java-native-community: '1'
requests:
- instrumentation.odigos.io/java-native-community: "1"
+ instrumentation.odigos.io/java-native-community: '1'
status:
containerStatuses:
- name: frontend
@@ -213,9 +213,9 @@ spec:
- name: coupon
resources:
limits:
- instrumentation.odigos.io/javascript-native-community: "1"
+ instrumentation.odigos.io/javascript-native-community: '1'
requests:
- instrumentation.odigos.io/javascript-native-community: "1"
+ instrumentation.odigos.io/javascript-native-community: '1'
status:
containerStatuses:
- name: coupon
@@ -235,9 +235,9 @@ spec:
- name: inventory
resources:
limits:
- instrumentation.odigos.io/python-native-community: "1"
+ instrumentation.odigos.io/python-native-community: '1'
requests:
- instrumentation.odigos.io/python-native-community: "1"
+ instrumentation.odigos.io/python-native-community: '1'
status:
containerStatuses:
- name: inventory
@@ -257,9 +257,9 @@ spec:
- name: membership
resources:
limits:
- instrumentation.odigos.io/go-ebpf-community: "1"
+ instrumentation.odigos.io/go-ebpf-community: '1'
requests:
- instrumentation.odigos.io/go-ebpf-community: "1"
+ instrumentation.odigos.io/go-ebpf-community: '1'
status:
containerStatuses:
- name: membership
@@ -279,9 +279,9 @@ spec:
- name: pricing
resources:
limits:
- instrumentation.odigos.io/dotnet-native-community: "1"
+ instrumentation.odigos.io/dotnet-native-community: '1'
requests:
- instrumentation.odigos.io/dotnet-native-community: "1"
+ instrumentation.odigos.io/dotnet-native-community: '1'
status:
containerStatuses:
- name: pricing
@@ -314,8 +314,7 @@ status:
- key: k8s.container.name
(value != null): true
- key: k8s.pod.name
- (value != null): true
----
+ (value != null): true
apiVersion: odigos.io/v1alpha1
kind: InstrumentationInstance
metadata:
@@ -337,7 +336,6 @@ status:
(value != null): true
- key: k8s.pod.name
(value != null): true
----
apiVersion: odigos.io/v1alpha1
kind: InstrumentationInstance
metadata: