mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-10 06:00:19 +01:00
Compare commits
3 Commits
refactor/c
...
feat/trace
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4cd6a9687 | ||
|
|
e543776efc | ||
|
|
621127b7fb |
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/cmd"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
|
||||
@@ -93,6 +94,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return noopgateway.NewProviderFactory()
|
||||
},
|
||||
func(_ licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
|
||||
return signoz.NewAuditorProviderFactories()
|
||||
},
|
||||
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||
return querier.NewHandler(ps, q, a)
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/SigNoz/signoz/cmd"
|
||||
"github.com/SigNoz/signoz/ee/auditor/otlphttpauditor"
|
||||
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
|
||||
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
|
||||
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
|
||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -133,6 +135,13 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
|
||||
return httpgateway.NewProviderFactory(licensing)
|
||||
},
|
||||
func(licensing licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
|
||||
factories := signoz.NewAuditorProviderFactories()
|
||||
if err := factories.Add(otlphttpauditor.NewFactory(licensing, version.Info)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return factories
|
||||
},
|
||||
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||
communityHandler := querier.NewHandler(ps, q, a)
|
||||
return eequerier.NewHandler(ps, q, communityHandler)
|
||||
|
||||
@@ -364,3 +364,34 @@ serviceaccount:
|
||||
analytics:
|
||||
# toggle service account analytics
|
||||
enabled: true
|
||||
|
||||
##################### Auditor #####################
|
||||
auditor:
|
||||
# Specifies the auditor provider to use.
|
||||
# noop: discards all audit events (community default).
|
||||
# otlphttp: exports audit events via OTLP HTTP (enterprise).
|
||||
provider: noop
|
||||
# The async channel capacity for audit events. Events are dropped when full (fail-open).
|
||||
buffer_size: 1000
|
||||
# The maximum number of events per export batch.
|
||||
batch_size: 100
|
||||
# The maximum time between export flushes.
|
||||
flush_interval: 1s
|
||||
otlphttp:
|
||||
# The target scheme://host:port/path of the OTLP HTTP endpoint.
|
||||
endpoint: http://localhost:4318/v1/logs
|
||||
# Whether to use HTTP instead of HTTPS.
|
||||
insecure: false
|
||||
# The maximum duration for an export attempt.
|
||||
timeout: 10s
|
||||
# Additional HTTP headers sent with every export request.
|
||||
headers: {}
|
||||
retry:
|
||||
# Whether to retry on transient failures.
|
||||
enabled: true
|
||||
# The initial wait time before the first retry.
|
||||
initial_interval: 5s
|
||||
# The upper bound on backoff interval.
|
||||
max_interval: 30s
|
||||
# The total maximum time spent retrying.
|
||||
max_elapsed_time: 60s
|
||||
|
||||
@@ -227,7 +227,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
s.config.APIServer.Timeout.Default,
|
||||
s.config.APIServer.Timeout.Max,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap)
|
||||
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, s.signoz.Auditor).Wrap)
|
||||
r.Use(middleware.NewComment().Wrap)
|
||||
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
|
||||
@@ -139,10 +139,12 @@
|
||||
"react-helmet-async": "1.3.0",
|
||||
"react-hook-form": "7.71.2",
|
||||
"react-i18next": "^11.16.1",
|
||||
"react-json-tree": "0.20.0",
|
||||
"react-lottie": "1.2.10",
|
||||
"react-markdown": "8.0.7",
|
||||
"react-query": "3.39.3",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-rnd": "10.5.3",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-dom-v5-compat": "6.27.0",
|
||||
"react-syntax-highlighter": "15.5.0",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
.details-header {
|
||||
// ghost + secondary missing hover bg token in @signozhq/button
|
||||
--button-ghost-hover-background: var(--l3-background);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
height: 36px;
|
||||
background: var(--l2-background);
|
||||
|
||||
&__icon-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--l1-foreground);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { X } from '@signozhq/icons';
|
||||
|
||||
import './DetailsHeader.styles.scss';
|
||||
|
||||
export interface HeaderAction {
|
||||
key: string;
|
||||
component: ReactNode; // check later if we can use direct btn itself or not.
|
||||
}
|
||||
|
||||
export interface DetailsHeaderProps {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
actions?: HeaderAction[];
|
||||
closePosition?: 'left' | 'right';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function DetailsHeader({
|
||||
title,
|
||||
onClose,
|
||||
actions,
|
||||
closePosition = 'right',
|
||||
className,
|
||||
}: DetailsHeaderProps): JSX.Element {
|
||||
const closeButton = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
className="details-header__icon-btn"
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`details-header ${className || ''}`}>
|
||||
{closePosition === 'left' && closeButton}
|
||||
|
||||
<span className="details-header__title">{title}</span>
|
||||
|
||||
{actions && (
|
||||
<div className="details-header__actions">
|
||||
{actions.map((action) => (
|
||||
<div key={action.key}>{action.component}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{closePosition === 'right' && closeButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailsHeader;
|
||||
@@ -0,0 +1,7 @@
|
||||
.details-panel-drawer {
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
36
frontend/src/components/DetailsPanel/DetailsPanelDrawer.tsx
Normal file
36
frontend/src/components/DetailsPanel/DetailsPanelDrawer.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { DrawerWrapper } from '@signozhq/drawer';
|
||||
|
||||
import './DetailsPanelDrawer.styles.scss';
|
||||
|
||||
interface DetailsPanelDrawerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function DetailsPanelDrawer({
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
className,
|
||||
}: DetailsPanelDrawerProps): JSX.Element {
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={isOpen}
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
type="panel"
|
||||
showOverlay={false}
|
||||
allowOutsideClick
|
||||
className={`details-panel-drawer ${className || ''}`}
|
||||
content={<div className="details-panel-drawer__body">{children}</div>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailsPanelDrawer;
|
||||
8
frontend/src/components/DetailsPanel/index.ts
Normal file
8
frontend/src/components/DetailsPanel/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type {
|
||||
DetailsHeaderProps,
|
||||
HeaderAction,
|
||||
} from './DetailsHeader/DetailsHeader';
|
||||
export { default as DetailsHeader } from './DetailsHeader/DetailsHeader';
|
||||
export { default as DetailsPanelDrawer } from './DetailsPanelDrawer';
|
||||
export type { DetailsPanelState, UseDetailsPanelOptions } from './types';
|
||||
export { default as useDetailsPanel } from './useDetailsPanel';
|
||||
10
frontend/src/components/DetailsPanel/types.ts
Normal file
10
frontend/src/components/DetailsPanel/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface DetailsPanelState {
|
||||
isOpen: boolean;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export interface UseDetailsPanelOptions {
|
||||
entityId: string | undefined;
|
||||
onClose?: () => void;
|
||||
}
|
||||
29
frontend/src/components/DetailsPanel/useDetailsPanel.ts
Normal file
29
frontend/src/components/DetailsPanel/useDetailsPanel.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { DetailsPanelState, UseDetailsPanelOptions } from './types';
|
||||
|
||||
function useDetailsPanel({
|
||||
entityId,
|
||||
onClose,
|
||||
}: UseDetailsPanelOptions): DetailsPanelState {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const prevEntityIdRef = useRef<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const currentId = entityId || '';
|
||||
if (currentId && currentId !== prevEntityIdRef.current) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
prevEntityIdRef.current = currentId;
|
||||
}, [entityId]);
|
||||
|
||||
const open = useCallback(() => setIsOpen(true), []);
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
return { isOpen, open, close };
|
||||
}
|
||||
|
||||
export default useDetailsPanel;
|
||||
@@ -677,6 +677,18 @@ function NewWidget({
|
||||
queryType: currentQuery.queryType,
|
||||
isNewPanel,
|
||||
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
|
||||
...(currentQuery.queryType === EQueryType.CLICKHOUSE && {
|
||||
clickhouseQueryCount: currentQuery.clickhouse_sql.length,
|
||||
clickhouseQueries: currentQuery.clickhouse_sql.map((q) => ({
|
||||
name: q.name,
|
||||
query: (q.query ?? '')
|
||||
.replace(/--[^\n]*/g, '') // strip line comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // strip block comments
|
||||
.replace(/'(?:[^'\\]|\\.|'')*'/g, "'?'") // replace single-quoted strings (handles \' and '' escapes)
|
||||
.replace(/\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/g, '?'), // replace numeric literals (int, float, scientific)
|
||||
disabled: q.disabled,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
setSaveModal(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.data-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
// Toolbar — view mode switcher + copy button
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
&__mode-select {
|
||||
min-width: 90px;
|
||||
height: 28px !important;
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 4px !important;
|
||||
border-color: var(--l2-border) !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--l2-foreground);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
// Shared content container — no scroll, each view handles its own
|
||||
&__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
67
frontend/src/periscope/components/DataViewer/DataViewer.tsx
Normal file
67
frontend/src/periscope/components/DataViewer/DataViewer.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Copy } from '@signozhq/icons';
|
||||
// TODO: Replace antd Select with @signozhq/ui component when moving to design library
|
||||
import { Select } from 'antd';
|
||||
import { JsonView } from 'periscope/components/JsonView';
|
||||
import { PrettyView } from 'periscope/components/PrettyView';
|
||||
import { PrettyViewProps } from 'periscope/components/PrettyView';
|
||||
|
||||
import { copyToClipboard } from './utils';
|
||||
|
||||
import './DataViewer.styles.scss';
|
||||
|
||||
type ViewMode = 'pretty' | 'json';
|
||||
|
||||
export interface DataViewerProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: Record<string, any>;
|
||||
drawerKey?: string;
|
||||
prettyViewProps?: Omit<PrettyViewProps, 'data' | 'drawerKey'>;
|
||||
}
|
||||
|
||||
function DataViewer({
|
||||
data,
|
||||
drawerKey = 'default',
|
||||
prettyViewProps,
|
||||
}: DataViewerProps): JSX.Element {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('pretty');
|
||||
|
||||
const jsonString = useMemo(() => JSON.stringify(data, null, 2), [data]);
|
||||
|
||||
return (
|
||||
<div className="data-viewer">
|
||||
<div className="data-viewer__toolbar">
|
||||
<Select
|
||||
className="data-viewer__mode-select"
|
||||
size="small"
|
||||
value={viewMode}
|
||||
onChange={(value: ViewMode): void => setViewMode(value)}
|
||||
options={[
|
||||
{ label: 'Pretty', value: 'pretty' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
]}
|
||||
getPopupContainer={(trigger): HTMLElement =>
|
||||
trigger.parentElement || document.body
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="data-viewer__copy-btn"
|
||||
onClick={(): void => copyToClipboard(data)}
|
||||
aria-label="Copy JSON"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="data-viewer__content">
|
||||
{viewMode === 'pretty' && (
|
||||
<PrettyView data={data} drawerKey={drawerKey} {...prettyViewProps} />
|
||||
)}
|
||||
{viewMode === 'json' && <JsonView data={jsonString} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataViewer;
|
||||
2
frontend/src/periscope/components/DataViewer/index.ts
Normal file
2
frontend/src/periscope/components/DataViewer/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type { DataViewerProps } from './DataViewer';
|
||||
export { default as DataViewer } from './DataViewer';
|
||||
12
frontend/src/periscope/components/DataViewer/utils.ts
Normal file
12
frontend/src/periscope/components/DataViewer/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { toast } from '@signozhq/sonner';
|
||||
|
||||
export function copyToClipboard(value: unknown): void {
|
||||
const text =
|
||||
typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
navigator.clipboard.writeText(text);
|
||||
toast.success('Copied to clipboard', {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
.floating-panel {
|
||||
z-index: 999;
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
overflow: hidden;
|
||||
|
||||
// Inner div fills the Rnd-controlled size
|
||||
&__inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Drag handle — any child with this class becomes the drag target
|
||||
.floating-panel__drag-handle {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ComponentProps } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Rnd } from 'react-rnd';
|
||||
|
||||
import './FloatingPanel.styles.scss';
|
||||
|
||||
type EnableResizing = ComponentProps<typeof Rnd>['enableResizing'];
|
||||
|
||||
export interface FloatingPanelProps {
|
||||
isOpen: boolean;
|
||||
children: React.ReactNode;
|
||||
defaultPosition?: { x: number; y: number };
|
||||
width?: number;
|
||||
height?: number;
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
enableResizing?: EnableResizing;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function FloatingPanel({
|
||||
isOpen,
|
||||
children,
|
||||
defaultPosition,
|
||||
width = 560,
|
||||
height = 600,
|
||||
minWidth = 400,
|
||||
minHeight = 300,
|
||||
enableResizing,
|
||||
className,
|
||||
}: FloatingPanelProps): JSX.Element | null {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialPosition = defaultPosition || {
|
||||
x: window.innerWidth - width - 24,
|
||||
y: 80,
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<Rnd
|
||||
default={{
|
||||
x: initialPosition.x,
|
||||
y: initialPosition.y,
|
||||
width,
|
||||
height,
|
||||
}}
|
||||
dragHandleClassName="floating-panel__drag-handle"
|
||||
minWidth={minWidth}
|
||||
minHeight={minHeight}
|
||||
onDrag={(_e, d): void | false => {
|
||||
const HEADER_HEIGHT = 40;
|
||||
// Top: don't allow header to go above viewport
|
||||
if (d.y < 0) {
|
||||
return false;
|
||||
}
|
||||
// Left: don't allow panel to go off-screen left
|
||||
if (d.x < 0) {
|
||||
return false;
|
||||
}
|
||||
// Bottom: only header needs to be visible
|
||||
if (d.y > window.innerHeight - HEADER_HEIGHT) {
|
||||
return false;
|
||||
}
|
||||
// Right: at least the close button (~40px) stays visible
|
||||
if (d.x > window.innerWidth - 40) {
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
className={`floating-panel ${className || ''}`}
|
||||
enableResizing={enableResizing}
|
||||
>
|
||||
<div className="floating-panel__inner">{children}</div>
|
||||
</Rnd>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
export default FloatingPanel;
|
||||
2
frontend/src/periscope/components/FloatingPanel/index.ts
Normal file
2
frontend/src/periscope/components/FloatingPanel/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type { FloatingPanelProps } from './FloatingPanel';
|
||||
export { default as FloatingPanel } from './FloatingPanel';
|
||||
@@ -0,0 +1,22 @@
|
||||
.json-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&__footer {
|
||||
flex-shrink: 0;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
&__wrap-toggle {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
78
frontend/src/periscope/components/JsonView/JsonView.tsx
Normal file
78
frontend/src/periscope/components/JsonView/JsonView.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Switch, Typography } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import './JsonView.styles.scss';
|
||||
|
||||
export interface JsonViewProps {
|
||||
data: string;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
const editorOptions: EditorProps['options'] = {
|
||||
automaticLayout: true,
|
||||
readOnly: true,
|
||||
wordWrap: 'on',
|
||||
minimap: { enabled: false },
|
||||
fontWeight: 400,
|
||||
fontFamily: 'SF Mono, Geist Mono, Fira Code, monospace',
|
||||
fontSize: 12,
|
||||
lineHeight: '18px',
|
||||
colorDecorators: true,
|
||||
scrollBeyondLastLine: false,
|
||||
decorationsOverviewRuler: false,
|
||||
scrollbar: { vertical: 'hidden', horizontal: 'hidden' },
|
||||
folding: false,
|
||||
};
|
||||
|
||||
function setEditorTheme(monaco: Monaco): void {
|
||||
monaco.editor.defineTheme('signoz-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
|
||||
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
|
||||
],
|
||||
colors: {
|
||||
'editor.background': '#00000000', // transparent
|
||||
},
|
||||
fontFamily: 'SF Mono, Geist Mono, Fira Code, monospace',
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
lineHeight: 18,
|
||||
letterSpacing: -0.06,
|
||||
});
|
||||
}
|
||||
|
||||
function JsonView({ data, height = '575px' }: JsonViewProps): JSX.Element {
|
||||
const [isWrapWord, setIsWrapWord] = useState(true);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
return (
|
||||
<div className="json-view">
|
||||
<MEditor
|
||||
value={data}
|
||||
language="json"
|
||||
options={{ ...editorOptions, wordWrap: isWrapWord ? 'on' : 'off' }}
|
||||
onChange={(): void => {}}
|
||||
height={height}
|
||||
theme={isDarkMode ? 'signoz-dark' : 'light'}
|
||||
beforeMount={setEditorTheme}
|
||||
/>
|
||||
<div className="json-view__footer">
|
||||
<div className="json-view__wrap-toggle">
|
||||
<Typography.Text>Wrap text</Typography.Text>
|
||||
<Switch
|
||||
checked={isWrapWord}
|
||||
onChange={(checked): void => setIsWrapWord(checked)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JsonView;
|
||||
2
frontend/src/periscope/components/JsonView/index.ts
Normal file
2
frontend/src/periscope/components/JsonView/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type { JsonViewProps } from './JsonView';
|
||||
export { default as JsonView } from './JsonView';
|
||||
@@ -1,43 +1,67 @@
|
||||
.key-value-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 2px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&--row {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&--column {
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
|
||||
.key-value-label__key {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.48px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.key-value-label__value {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__key,
|
||||
&__value {
|
||||
padding: 1px 6px;
|
||||
padding: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.005em;
|
||||
&,
|
||||
&:hover {
|
||||
color: var(--text-vanilla-400);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__key {
|
||||
background: var(--bg-ink-400);
|
||||
background: var(--l2-background);
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
&__value {
|
||||
background: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.key-value-label {
|
||||
border-color: var(--bg-vanilla-400);
|
||||
&__key,
|
||||
&__value {
|
||||
color: var(--text-ink-400);
|
||||
}
|
||||
&__key {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
&__value {
|
||||
background: var(--bg-vanilla-200);
|
||||
&__value {
|
||||
background: var(--l3-background);
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,61 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
import TrimmedText from '../TrimmedText/TrimmedText';
|
||||
|
||||
import './KeyValueLabel.styles.scss';
|
||||
|
||||
// Rethink this component later
|
||||
type KeyValueLabelProps = {
|
||||
badgeKey: string | React.ReactNode;
|
||||
badgeValue: string;
|
||||
badgeValue: string | React.ReactNode;
|
||||
direction?: 'row' | 'column';
|
||||
maxCharacters?: number;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export default function KeyValueLabel({
|
||||
badgeKey,
|
||||
badgeValue,
|
||||
direction = 'row',
|
||||
maxCharacters = 20,
|
||||
onClick,
|
||||
}: KeyValueLabelProps): JSX.Element | null {
|
||||
const isUrl = useMemo(() => /^https?:\/\//.test(badgeValue), [badgeValue]);
|
||||
|
||||
if (!badgeKey || !badgeValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderValue = (): JSX.Element => {
|
||||
if (typeof badgeValue !== 'string') {
|
||||
return <div className="key-value-label__value">{badgeValue}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={badgeValue}>
|
||||
<div
|
||||
className={`key-value-label__value ${
|
||||
onClick ? 'key-value-label__value--clickable' : ''
|
||||
}`}
|
||||
onClick={onClick}
|
||||
role={onClick ? 'button' : undefined}
|
||||
tabIndex={onClick ? 0 : undefined}
|
||||
onKeyDown={
|
||||
onClick
|
||||
? (e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onClick();
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<TrimmedText text={badgeValue} maxCharacters={maxCharacters} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="key-value-label">
|
||||
<div className={`key-value-label key-value-label--${direction}`}>
|
||||
<div className="key-value-label__key">
|
||||
{typeof badgeKey === 'string' ? (
|
||||
<TrimmedText text={badgeKey} maxCharacters={maxCharacters} />
|
||||
@@ -31,26 +63,13 @@ export default function KeyValueLabel({
|
||||
badgeKey
|
||||
)}
|
||||
</div>
|
||||
{isUrl ? (
|
||||
<a
|
||||
href={badgeValue}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="key-value-label__value"
|
||||
>
|
||||
<TrimmedText text={badgeValue} maxCharacters={maxCharacters} />
|
||||
</a>
|
||||
) : (
|
||||
<Tooltip title={badgeValue}>
|
||||
<div className="key-value-label__value">
|
||||
<TrimmedText text={badgeValue} maxCharacters={maxCharacters} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{renderValue()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
KeyValueLabel.defaultProps = {
|
||||
maxCharacters: 20,
|
||||
direction: 'row',
|
||||
onClick: undefined,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
.pretty-view {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__search-input {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--l2-border) !important;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', 'Geist Mono', 'Fira Code', monospace !important;
|
||||
font-size: 12px !important;
|
||||
line-height: 18px !important;
|
||||
background: var(--l1-background) !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary) !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Font spec: SF Mono, 12px, 400, 18px line-height, -0.5% letter-spacing
|
||||
font-family: 'SF Mono', 'Geist Mono', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
|
||||
// Override react-json-tree inline styles
|
||||
ul {
|
||||
margin: 0 !important;
|
||||
padding-left: 16px !important;
|
||||
list-style: none !important;
|
||||
background-color: transparent !important;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
> ul,
|
||||
&__pinned > ul {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
// Force font on all tree elements
|
||||
li,
|
||||
span,
|
||||
label {
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
line-height: inherit !important;
|
||||
letter-spacing: inherit !important;
|
||||
}
|
||||
|
||||
// Fix react-json-tree text-indent wrapping
|
||||
li {
|
||||
text-indent: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
padding-top: 2px !important;
|
||||
padding-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.pretty-view__actions {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
// Leaf node row — hover highlights only this row
|
||||
&__row {
|
||||
border-radius: 2px;
|
||||
display: flex !important;
|
||||
align-items: baseline;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--l3-background);
|
||||
|
||||
.pretty-view__actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Push actions to the right edge
|
||||
> span {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Nested node (object/array) — hover only on the label line, not children
|
||||
&__nested-row {
|
||||
> label,
|
||||
> span:not(ul span) {
|
||||
border-radius: 2px;
|
||||
padding: 1px 2px;
|
||||
display: inline !important; // keep item string inline with label
|
||||
}
|
||||
|
||||
// Highlight label + item string on hover, show actions
|
||||
&:hover > label,
|
||||
&:hover > label + span {
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
|
||||
&:hover > label + span .pretty-view__actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// In nested rows, value-row should not take full width
|
||||
.pretty-view__value-row {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Value + actions wrapper (from valueRenderer / getItemString)
|
||||
&__value-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
// In leaf rows, value-row takes full width to push ... to the right
|
||||
&__row &__value-row {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// ... actions button — hidden by default, shown on row hover
|
||||
&__actions {
|
||||
opacity: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--l2-foreground);
|
||||
transition: opacity 0.1s ease;
|
||||
padding: 0 2px;
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
// Pinned items section
|
||||
&__pinned {
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 0px;
|
||||
|
||||
li {
|
||||
margin-left: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
display: flex !important;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
&__pinned-header {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--l3-foreground);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
&__pinned-label {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__pinned-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-robin-400);
|
||||
cursor: pointer;
|
||||
fill: var(--text-robin-400);
|
||||
transition: fill 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-robin-300);
|
||||
fill: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
300
frontend/src/periscope/components/PrettyView/PrettyView.tsx
Normal file
300
frontend/src/periscope/components/PrettyView/PrettyView.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { JSONTree, KeyPath } from 'react-json-tree';
|
||||
import { Copy, Ellipsis, Pin, PinOff } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import type { MenuProps } from 'antd';
|
||||
// TODO: Replace antd Dropdown with @signozhq/ui component when moving to design library
|
||||
import { Dropdown } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import { darkTheme, lightTheme, themeExtension } from './constants';
|
||||
import usePinnedFields from './hooks/usePinnedFields';
|
||||
import useSearchFilter, { filterTree } from './hooks/useSearchFilter';
|
||||
import {
|
||||
copyToClipboard,
|
||||
keyPathToDisplayString,
|
||||
keyPathToForward,
|
||||
serializeKeyPath,
|
||||
} from './utils';
|
||||
|
||||
import './PrettyView.styles.scss';
|
||||
|
||||
export interface FieldContext {
|
||||
fieldKey: string;
|
||||
fieldKeyPath: (string | number)[];
|
||||
fieldValue: unknown;
|
||||
isNested: boolean;
|
||||
}
|
||||
|
||||
export interface PrettyViewAction {
|
||||
key: string;
|
||||
label: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
onClick: (context: FieldContext) => void;
|
||||
}
|
||||
|
||||
export interface PrettyViewProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: Record<string, any>;
|
||||
actions?: PrettyViewAction[];
|
||||
searchable?: boolean;
|
||||
showPinned?: boolean;
|
||||
drawerKey?: string;
|
||||
}
|
||||
|
||||
function PrettyView({
|
||||
data,
|
||||
actions,
|
||||
searchable = true,
|
||||
showPinned = false,
|
||||
drawerKey = 'default',
|
||||
}: PrettyViewProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { searchQuery, setSearchQuery, filteredData } = useSearchFilter(data);
|
||||
const {
|
||||
isPinned,
|
||||
togglePin,
|
||||
pinnedEntries,
|
||||
pinnedData,
|
||||
displayKeyToForwardPath,
|
||||
} = usePinnedFields(data, drawerKey);
|
||||
|
||||
const filteredPinnedData = useMemo(() => {
|
||||
const trimmed = searchQuery.trim();
|
||||
if (!trimmed) {
|
||||
return pinnedData;
|
||||
}
|
||||
return filterTree(pinnedData, trimmed) || {};
|
||||
}, [pinnedData, searchQuery]);
|
||||
|
||||
const theme = useMemo(
|
||||
() => ({
|
||||
extend: isDarkMode ? darkTheme : lightTheme,
|
||||
...themeExtension,
|
||||
}),
|
||||
[isDarkMode],
|
||||
);
|
||||
|
||||
const shouldExpandNodeInitially = useCallback(
|
||||
(
|
||||
_keyPath: readonly (string | number)[],
|
||||
_data: unknown,
|
||||
level: number,
|
||||
): boolean => level < 5,
|
||||
[],
|
||||
);
|
||||
|
||||
const buildMenuItems = useCallback(
|
||||
(context: FieldContext): MenuProps['items'] => {
|
||||
// todo: drive dropdown through config.
|
||||
const copyItem = {
|
||||
key: 'copy',
|
||||
label: 'Copy',
|
||||
icon: <Copy size={12} />,
|
||||
onClick: (): void => {
|
||||
copyToClipboard(context.fieldValue);
|
||||
},
|
||||
};
|
||||
|
||||
const items: NonNullable<MenuProps['items']> = [copyItem];
|
||||
|
||||
// Pin action only for leaf nodes
|
||||
if (!context.isNested) {
|
||||
// Resolve the correct forward path — pinned tree uses display keys
|
||||
// which don't match the original serialized path
|
||||
const resolvedPath =
|
||||
displayKeyToForwardPath[context.fieldKey] || context.fieldKeyPath;
|
||||
const serialized = serializeKeyPath(resolvedPath);
|
||||
const pinned = isPinned(serialized);
|
||||
|
||||
items.push({
|
||||
key: 'pin',
|
||||
label: pinned ? 'Unpin field' : 'Pin field',
|
||||
icon: pinned ? <PinOff size={12} /> : <Pin size={12} />,
|
||||
onClick: (): void => {
|
||||
togglePin(resolvedPath);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (actions && actions.length > 0) {
|
||||
//todo: why this divider?
|
||||
items.push({ type: 'divider' as const, key: 'divider' });
|
||||
actions.forEach((action) => {
|
||||
items.push({
|
||||
key: action.key,
|
||||
label: action.label,
|
||||
icon: action.icon,
|
||||
onClick: (): void => {
|
||||
action.onClick(context);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
[actions, isPinned, togglePin, displayKeyToForwardPath],
|
||||
);
|
||||
|
||||
const renderWithActions = useCallback(
|
||||
({
|
||||
content,
|
||||
fieldKey,
|
||||
fieldKeyPath,
|
||||
value,
|
||||
isNested,
|
||||
}: {
|
||||
content: React.ReactNode;
|
||||
fieldKey: string;
|
||||
fieldKeyPath: (string | number)[];
|
||||
value: unknown;
|
||||
isNested: boolean;
|
||||
}): React.ReactNode => {
|
||||
const context: FieldContext = {
|
||||
fieldKey,
|
||||
fieldKeyPath,
|
||||
fieldValue: value,
|
||||
isNested,
|
||||
};
|
||||
const menuItems = buildMenuItems(context);
|
||||
return (
|
||||
<span className="pretty-view__value-row">
|
||||
<span>{content}</span>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: menuItems,
|
||||
onClick: (e): void => {
|
||||
e.domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="bottomLeft"
|
||||
getPopupContainer={(trigger): HTMLElement =>
|
||||
trigger.parentElement || document.body
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="pretty-view__actions"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Ellipsis size={12} />
|
||||
</span>
|
||||
</Dropdown>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
[buildMenuItems],
|
||||
);
|
||||
|
||||
const getItemString = useCallback(
|
||||
(
|
||||
_nodeType: string,
|
||||
nodeData: unknown,
|
||||
itemType: React.ReactNode,
|
||||
itemString: string,
|
||||
keyPath: KeyPath,
|
||||
): // eslint-disable-next-line max-params
|
||||
React.ReactNode => {
|
||||
const forwardPath = keyPathToForward(keyPath);
|
||||
return renderWithActions({
|
||||
content: (
|
||||
<>
|
||||
{itemType} {itemString}
|
||||
</>
|
||||
),
|
||||
fieldKey: keyPathToDisplayString(keyPath),
|
||||
fieldKeyPath: forwardPath,
|
||||
value: nodeData,
|
||||
isNested: true,
|
||||
});
|
||||
},
|
||||
[renderWithActions],
|
||||
);
|
||||
|
||||
const valueRenderer = useCallback(
|
||||
(
|
||||
valueAsString: unknown,
|
||||
value: unknown,
|
||||
...keyPath: KeyPath
|
||||
): React.ReactNode => {
|
||||
const forwardPath = keyPathToForward(keyPath);
|
||||
return renderWithActions({
|
||||
content: String(valueAsString),
|
||||
fieldKey: keyPathToDisplayString(keyPath),
|
||||
fieldKeyPath: forwardPath,
|
||||
value,
|
||||
isNested: typeof value === 'object' && value !== null,
|
||||
});
|
||||
},
|
||||
[renderWithActions],
|
||||
);
|
||||
|
||||
const pinnedLabelRenderer = useCallback(
|
||||
(keyPath: KeyPath): React.ReactNode => {
|
||||
const displayKey = String(keyPath[0]);
|
||||
const entry = pinnedEntries.find((e) => e.displayKey === displayKey);
|
||||
return (
|
||||
<span className="pretty-view__pinned-label">
|
||||
<Pin
|
||||
size={12}
|
||||
className="pretty-view__pinned-icon"
|
||||
onClick={(): void => {
|
||||
if (entry) {
|
||||
togglePin(entry.forwardPath);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>{displayKey}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
[togglePin, pinnedEntries],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pretty-view">
|
||||
{searchable && (
|
||||
<Input
|
||||
className="pretty-view__search-input"
|
||||
type="text"
|
||||
placeholder="Search for a field..."
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showPinned && Object.keys(filteredPinnedData).length > 0 && (
|
||||
<div className="pretty-view__pinned">
|
||||
<div className="pretty-view__pinned-header">PINNED ITEMS</div>
|
||||
<JSONTree
|
||||
key={`pinned-${searchQuery}`}
|
||||
data={filteredPinnedData}
|
||||
theme={theme}
|
||||
invertTheme={false}
|
||||
hideRoot
|
||||
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
||||
valueRenderer={valueRenderer}
|
||||
getItemString={getItemString}
|
||||
labelRenderer={pinnedLabelRenderer}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<JSONTree
|
||||
key={searchQuery}
|
||||
data={filteredData}
|
||||
theme={theme}
|
||||
invertTheme={false}
|
||||
hideRoot
|
||||
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
||||
valueRenderer={valueRenderer}
|
||||
getItemString={getItemString}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PrettyView;
|
||||
62
frontend/src/periscope/components/PrettyView/constants.ts
Normal file
62
frontend/src/periscope/components/PrettyView/constants.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// Dark theme — SigNoz design palette
|
||||
export const darkTheme = {
|
||||
scheme: 'signoz-dark',
|
||||
author: 'signoz',
|
||||
base00: 'transparent',
|
||||
base01: '#161922',
|
||||
base02: '#1d212d',
|
||||
base03: '#62687C',
|
||||
base04: '#ADB4C2',
|
||||
base05: '#ADB4C2',
|
||||
base06: '#ADB4C2',
|
||||
base07: '#ADB4C2',
|
||||
base08: '#EA6D71',
|
||||
base09: '#7CEDBD',
|
||||
base0A: '#7CEDBD',
|
||||
base0B: '#ADB4C2',
|
||||
base0C: '#23C4F8',
|
||||
base0D: '#95ACFB',
|
||||
base0E: '#95ACFB',
|
||||
base0F: '#AD7F58',
|
||||
};
|
||||
|
||||
// Light theme
|
||||
export const lightTheme = {
|
||||
scheme: 'signoz-light',
|
||||
author: 'signoz',
|
||||
base00: 'transparent',
|
||||
base01: '#F9F9FB',
|
||||
base02: '#EFF0F3',
|
||||
base03: '#80828D',
|
||||
base04: '#62636C',
|
||||
base05: '#62636C',
|
||||
base06: '#62636C',
|
||||
base07: '#1E1F24',
|
||||
base08: '#E5484D',
|
||||
base09: '#168757',
|
||||
base0A: '#168757',
|
||||
base0B: '#62636C',
|
||||
base0C: '#157594',
|
||||
base0D: '#2F48A0',
|
||||
base0E: '#2F48A0',
|
||||
base0F: '#684C35',
|
||||
};
|
||||
|
||||
export const themeExtension = {
|
||||
value: ({
|
||||
style,
|
||||
}: {
|
||||
style: Record<string, unknown>;
|
||||
}): { style: Record<string, unknown>; className: string } => ({
|
||||
style: { ...style },
|
||||
className: 'pretty-view__row',
|
||||
}),
|
||||
nestedNode: ({
|
||||
style,
|
||||
}: {
|
||||
style: Record<string, unknown>;
|
||||
}): { style: Record<string, unknown>; className: string } => ({
|
||||
style: { ...style },
|
||||
className: 'pretty-view__nested-row',
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
deserializeKeyPath,
|
||||
keyPathToDisplayString,
|
||||
resolveValueByKeys,
|
||||
serializeKeyPath,
|
||||
} from '../utils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyRecord = Record<string, any>;
|
||||
|
||||
const STORAGE_PREFIX = 'pinnedFields';
|
||||
|
||||
function loadFromStorage(storageKey: string): string[] {
|
||||
try {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveToStorage(storageKey: string, keys: string[]): void {
|
||||
localStorage.setItem(storageKey, JSON.stringify(keys));
|
||||
}
|
||||
|
||||
export interface PinnedEntry {
|
||||
serializedKey: string;
|
||||
displayKey: string;
|
||||
forwardPath: (string | number)[];
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export interface UsePinnedFieldsReturn {
|
||||
isPinned: (serializedKey: string) => boolean;
|
||||
togglePin: (forwardPath: (string | number)[]) => void;
|
||||
pinnedEntries: PinnedEntry[];
|
||||
pinnedData: AnyRecord;
|
||||
displayKeyToForwardPath: Record<string, (string | number)[]>;
|
||||
}
|
||||
|
||||
function usePinnedFields(
|
||||
data: AnyRecord,
|
||||
drawerKey: string,
|
||||
): UsePinnedFieldsReturn {
|
||||
const storageKey = `${STORAGE_PREFIX}:${drawerKey}`;
|
||||
|
||||
// Stored as serialized keyPath arrays (JSON strings)
|
||||
const [pinnedSerializedKeys, setPinnedSerializedKeys] = useState<Set<string>>(
|
||||
() => new Set(loadFromStorage(storageKey)),
|
||||
);
|
||||
|
||||
const togglePin = useCallback(
|
||||
(forwardPath: (string | number)[]): void => {
|
||||
const serialized = serializeKeyPath(forwardPath);
|
||||
setPinnedSerializedKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(serialized)) {
|
||||
next.delete(serialized);
|
||||
} else {
|
||||
next.add(serialized);
|
||||
}
|
||||
saveToStorage(storageKey, Array.from(next));
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[storageKey],
|
||||
);
|
||||
|
||||
const isPinned = useCallback(
|
||||
(serializedKey: string): boolean => pinnedSerializedKeys.has(serializedKey),
|
||||
[pinnedSerializedKeys],
|
||||
);
|
||||
|
||||
const pinnedEntries = useMemo(
|
||||
(): PinnedEntry[] =>
|
||||
Array.from(pinnedSerializedKeys)
|
||||
.map((serializedKey) => {
|
||||
const forwardPath = deserializeKeyPath(serializedKey);
|
||||
if (!forwardPath) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
serializedKey,
|
||||
displayKey: keyPathToDisplayString(
|
||||
[...forwardPath].reverse() as readonly (string | number)[],
|
||||
),
|
||||
forwardPath,
|
||||
value: resolveValueByKeys(data, forwardPath),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(entry): entry is PinnedEntry =>
|
||||
entry !== null && entry.value !== undefined,
|
||||
),
|
||||
[pinnedSerializedKeys, data],
|
||||
);
|
||||
|
||||
// Flat object for the pinned JSONTree — use display key as the object key
|
||||
const pinnedData = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
pinnedEntries.map((entry) => [entry.displayKey, entry.value]),
|
||||
),
|
||||
[pinnedEntries],
|
||||
);
|
||||
|
||||
// Map from display key to original forward path — for unpin from pinned tree
|
||||
const displayKeyToForwardPath = useMemo(
|
||||
(): Record<string, (string | number)[]> =>
|
||||
Object.fromEntries(
|
||||
pinnedEntries.map((entry) => [entry.displayKey, entry.forwardPath]),
|
||||
),
|
||||
[pinnedEntries],
|
||||
);
|
||||
|
||||
return {
|
||||
isPinned,
|
||||
togglePin,
|
||||
pinnedEntries,
|
||||
pinnedData,
|
||||
displayKeyToForwardPath,
|
||||
};
|
||||
}
|
||||
|
||||
export default usePinnedFields;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyRecord = Record<string, any>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyValue = any;
|
||||
|
||||
function isLeaf(value: unknown): boolean {
|
||||
return value === null || value === undefined || typeof value !== 'object';
|
||||
}
|
||||
|
||||
function matchesQuery(text: string, lowerQuery: string): boolean {
|
||||
return text.toLowerCase().includes(lowerQuery);
|
||||
}
|
||||
|
||||
// Filter a single value (leaf, array, or object) against the query.
|
||||
// Returns the filtered value or null if no match.
|
||||
function filterValue(value: AnyValue, query: string): AnyValue | null {
|
||||
if (isLeaf(value)) {
|
||||
return matchesQuery(String(value), query.toLowerCase()) ? value : null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return filterArray(value, query);
|
||||
}
|
||||
|
||||
return filterTree(value, query);
|
||||
}
|
||||
|
||||
// Recursively filter an array, keeping only elements that match
|
||||
function filterArray(arr: AnyValue[], query: string): AnyValue[] | null {
|
||||
const results = arr
|
||||
.map((item) => filterValue(item, query))
|
||||
.filter((item) => item !== null);
|
||||
|
||||
return results.length > 0 ? results : null;
|
||||
}
|
||||
|
||||
// Recursively filter the data tree, keeping only branches with matching keys or values
|
||||
export function filterTree(obj: AnyRecord, query: string): AnyRecord | null {
|
||||
const result: AnyRecord = {};
|
||||
const lowerQuery = query.toLowerCase();
|
||||
let hasMatch = false;
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (matchesQuery(key, lowerQuery)) {
|
||||
result[key] = value;
|
||||
hasMatch = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const filtered = filterValue(value, query);
|
||||
if (filtered !== null) {
|
||||
result[key] = filtered;
|
||||
hasMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasMatch ? result : null;
|
||||
}
|
||||
|
||||
interface UseSearchFilterReturn {
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
filteredData: AnyRecord;
|
||||
}
|
||||
|
||||
function useSearchFilter(data: AnyRecord): UseSearchFilterReturn {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
const trimmed = searchQuery.trim();
|
||||
if (!trimmed) {
|
||||
return data;
|
||||
}
|
||||
return filterTree(data, trimmed) || {};
|
||||
}, [data, searchQuery]);
|
||||
|
||||
return { searchQuery, setSearchQuery, filteredData };
|
||||
}
|
||||
|
||||
export default useSearchFilter;
|
||||
2
frontend/src/periscope/components/PrettyView/index.ts
Normal file
2
frontend/src/periscope/components/PrettyView/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type { PrettyViewProps } from './PrettyView';
|
||||
export { default as PrettyView } from './PrettyView';
|
||||
58
frontend/src/periscope/components/PrettyView/utils.ts
Normal file
58
frontend/src/periscope/components/PrettyView/utils.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { toast } from '@signozhq/sonner';
|
||||
|
||||
export function copyToClipboard(value: unknown): void {
|
||||
const text =
|
||||
typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
navigator.clipboard.writeText(text);
|
||||
toast.success('Copied to clipboard', {
|
||||
richColors: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve a value from a nested object using an array of keys (not dot-notation)
|
||||
// e.g. resolveValueByKeys({ tagMap: { 'cloud.account.id': 'x' } }, ['tagMap', 'cloud.account.id']) → 'x'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function resolveValueByKeys(
|
||||
data: Record<string, any>,
|
||||
keys: (string | number)[],
|
||||
): unknown {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return keys.reduce((obj: any, key) => obj?.[key], data);
|
||||
}
|
||||
|
||||
// Convert react-json-tree's reversed keyPath to forward order
|
||||
// e.g. ['cloud.account.id', 'tagMap'] → ['tagMap', 'cloud.account.id']
|
||||
export function keyPathToForward(
|
||||
keyPath: readonly (string | number)[],
|
||||
): (string | number)[] {
|
||||
return [...keyPath].reverse();
|
||||
}
|
||||
|
||||
// Display-friendly string for a keyPath
|
||||
// e.g. ['tagMap', 'cloud.account.id'] → 'tagMap.cloud.account.id'
|
||||
export function keyPathToDisplayString(
|
||||
keyPath: readonly (string | number)[],
|
||||
): string {
|
||||
return [...keyPath].reverse().join('.');
|
||||
}
|
||||
|
||||
// Serialize keyPath for storage/comparison (JSON stringified array)
|
||||
export function serializeKeyPath(keyPath: (string | number)[]): string {
|
||||
return JSON.stringify(keyPath);
|
||||
}
|
||||
|
||||
export function deserializeKeyPath(
|
||||
serialized: string,
|
||||
): (string | number)[] | null {
|
||||
try {
|
||||
const parsed = JSON.parse(serialized);
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
.resizable-box {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&--disabled {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__handle {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: var(--l2-border);
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
&--vertical {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
&--horizontal {
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
cursor: col-resize;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import './ResizableBox.styles.scss';
|
||||
|
||||
export interface ResizableBoxProps {
|
||||
children: React.ReactNode;
|
||||
direction?: 'vertical' | 'horizontal';
|
||||
defaultHeight?: number;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
defaultWidth?: number;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
onResize?: (size: number) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ResizableBox({
|
||||
children,
|
||||
direction = 'vertical',
|
||||
defaultHeight = 200,
|
||||
minHeight = 50,
|
||||
maxHeight = Infinity,
|
||||
defaultWidth = 200,
|
||||
minWidth = 50,
|
||||
maxWidth = Infinity,
|
||||
onResize,
|
||||
disabled = false,
|
||||
className,
|
||||
}: ResizableBoxProps): JSX.Element {
|
||||
const isHorizontal = direction === 'horizontal';
|
||||
const [size, setSize] = useState(isHorizontal ? defaultWidth : defaultHeight);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
const startPos = isHorizontal ? e.clientX : e.clientY;
|
||||
const startSize = size;
|
||||
const min = isHorizontal ? minWidth : minHeight;
|
||||
const max = isHorizontal ? maxWidth : maxHeight;
|
||||
|
||||
const onMouseMove = (moveEvent: MouseEvent): void => {
|
||||
const currentPos = isHorizontal ? moveEvent.clientX : moveEvent.clientY;
|
||||
const delta = currentPos - startPos;
|
||||
const newSize = Math.min(max, Math.max(min, startSize + delta));
|
||||
setSize(newSize);
|
||||
onResize?.(newSize);
|
||||
};
|
||||
|
||||
const onMouseUp = (): void => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
|
||||
document.body.style.cursor = isHorizontal ? 'col-resize' : 'row-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
},
|
||||
[size, isHorizontal, minWidth, maxWidth, minHeight, maxHeight, onResize],
|
||||
);
|
||||
|
||||
const containerStyle = disabled
|
||||
? undefined
|
||||
: isHorizontal
|
||||
? { width: size }
|
||||
: { height: size };
|
||||
const handleClass = `resizable-box__handle resizable-box__handle--${direction}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`resizable-box ${disabled ? 'resizable-box--disabled' : ''} ${
|
||||
className || ''
|
||||
}`}
|
||||
style={containerStyle}
|
||||
>
|
||||
<div className="resizable-box__content">{children}</div>
|
||||
{!disabled && <div className={handleClass} onMouseDown={handleMouseDown} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResizableBox;
|
||||
2
frontend/src/periscope/components/ResizableBox/index.ts
Normal file
2
frontend/src/periscope/components/ResizableBox/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type { ResizableBoxProps } from './ResizableBox';
|
||||
export { default as ResizableBox } from './ResizableBox';
|
||||
@@ -6538,6 +6538,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.0.tgz#d774355e41f372d5350a4d0714abb48194a489c3"
|
||||
integrity sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==
|
||||
|
||||
"@types/lodash@^4.17.0", "@types/lodash@^4.17.15":
|
||||
version "4.17.24"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.24.tgz#4ae334fc62c0e915ca8ed8e35dcc6d4eeb29215f"
|
||||
integrity sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==
|
||||
|
||||
"@types/mdast@^3.0.0":
|
||||
version "3.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.12.tgz#beeb511b977c875a5b0cc92eab6fcac2f0895514"
|
||||
@@ -8952,7 +8957,7 @@ color-string@^1.9.0:
|
||||
color-name "^1.0.0"
|
||||
simple-swizzle "^0.2.2"
|
||||
|
||||
color@^4.2.1:
|
||||
color@^4.2.1, color@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz"
|
||||
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
|
||||
@@ -9384,6 +9389,11 @@ csstype@^3.1.2:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||
|
||||
csstype@^3.1.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
|
||||
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
|
||||
|
||||
"d3-array@1 - 3", "d3-array@2 - 3", "d3-array@2.10.0 - 3":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.3.tgz"
|
||||
@@ -16819,6 +16829,11 @@ rc-virtual-list@^3.11.1, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2:
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.36.0"
|
||||
|
||||
re-resizable@^6.11.2:
|
||||
version "6.11.2"
|
||||
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.11.2.tgz#2e8f7119ca3881d5b5aea0ffa014a80e5c1252b3"
|
||||
integrity sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==
|
||||
|
||||
react-addons-update@15.6.3:
|
||||
version "15.6.3"
|
||||
resolved "https://registry.yarnpkg.com/react-addons-update/-/react-addons-update-15.6.3.tgz#c449c309154024d04087b206d0400e020547b313"
|
||||
@@ -16826,6 +16841,16 @@ react-addons-update@15.6.3:
|
||||
dependencies:
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react-base16-styling@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.10.0.tgz#5d5f019bd4dc5870c3e92fd9d5410533a0bbb0c6"
|
||||
integrity sha512-H1k2eFB6M45OaiRru3PBXkuCcn2qNmx+gzLb4a9IPMR7tMH8oBRXU5jGbPDYG1Hz+82d88ED0vjR8BmqU3pQdg==
|
||||
dependencies:
|
||||
"@types/lodash" "^4.17.0"
|
||||
color "^4.2.3"
|
||||
csstype "^3.1.3"
|
||||
lodash-es "^4.17.21"
|
||||
|
||||
react-beautiful-dnd@13.1.1:
|
||||
version "13.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2"
|
||||
@@ -16890,6 +16915,14 @@ react-draggable@^4.0.0, react-draggable@^4.0.3:
|
||||
clsx "^1.1.1"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-draggable@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.5.0.tgz#0b274ccb6965fcf97ed38fcf7e3cc223bc48cdf5"
|
||||
integrity sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==
|
||||
dependencies:
|
||||
clsx "^2.1.1"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-error-boundary@4.0.11:
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c"
|
||||
@@ -16980,6 +17013,14 @@ react-is@^18.3.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
|
||||
react-json-tree@0.20.0:
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/react-json-tree/-/react-json-tree-0.20.0.tgz#1e7fd26f093bd49d733f80dd9b6d40142e09f7c9"
|
||||
integrity sha512-h+f9fUNAxzBx1rbrgUF7+zSWKGHDtt2VPYLErIuB0JyKGnWgFMM21ksqQyb3EXwXNnoMW2rdE5kuAaubgGOx2Q==
|
||||
dependencies:
|
||||
"@types/lodash" "^4.17.15"
|
||||
react-base16-styling "^0.10.0"
|
||||
|
||||
react-kapsule@^2.5:
|
||||
version "2.5.7"
|
||||
resolved "https://registry.yarnpkg.com/react-kapsule/-/react-kapsule-2.5.7.tgz#dcd957ae8e897ff48055fc8ff48ed04ebe3c5bd2"
|
||||
@@ -17099,6 +17140,15 @@ react-resizable@^3.0.4:
|
||||
prop-types "15.x"
|
||||
react-draggable "^4.0.3"
|
||||
|
||||
react-rnd@10.5.3:
|
||||
version "10.5.3"
|
||||
resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.5.3.tgz#f8c484034276a561e8aa2fbf6a94580596621013"
|
||||
integrity sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q==
|
||||
dependencies:
|
||||
re-resizable "^6.11.2"
|
||||
react-draggable "^4.5.0"
|
||||
tslib "2.6.2"
|
||||
|
||||
react-router-dom-v5-compat@6.27.0:
|
||||
version "6.27.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.27.0.tgz#19429aefa130ee151e6f4487d073d919ec22d458"
|
||||
@@ -19184,6 +19234,11 @@ tsconfig-paths@^4.2.0:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@2.6.2, tslib@^2.0.0:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
tslib@2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
|
||||
@@ -19194,11 +19249,6 @@ tslib@^1.14.1, tslib@^1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz"
|
||||
|
||||
@@ -63,6 +63,7 @@ type RetryConfig struct {
|
||||
|
||||
func newConfig() factory.Config {
|
||||
return Config{
|
||||
Provider: "noop",
|
||||
BufferSize: 1000,
|
||||
BatchSize: 100,
|
||||
FlushInterval: time.Second,
|
||||
|
||||
@@ -208,7 +208,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
|
||||
s.config.APIServer.Timeout.Default,
|
||||
s.config.APIServer.Timeout.Max,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap)
|
||||
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, s.signoz.Auditor).Wrap)
|
||||
r.Use(middleware.NewComment().Wrap)
|
||||
|
||||
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/apiserver"
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/config"
|
||||
"github.com/SigNoz/signoz/pkg/emailing"
|
||||
@@ -123,6 +124,9 @@ type Config struct {
|
||||
|
||||
// ServiceAccount config
|
||||
ServiceAccount serviceaccount.Config `mapstructure:"serviceaccount"`
|
||||
|
||||
// Auditor config
|
||||
Auditor auditor.Config `mapstructure:"auditor"`
|
||||
}
|
||||
|
||||
func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.ResolverConfig) (Config, error) {
|
||||
@@ -153,6 +157,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
|
||||
user.NewConfigFactory(),
|
||||
identn.NewConfigFactory(),
|
||||
serviceaccount.NewConfigFactory(),
|
||||
auditor.NewConfigFactory(),
|
||||
}
|
||||
|
||||
conf, err := config.New(ctx, resolverConfig, configFactories)
|
||||
|
||||
@@ -3,6 +3,8 @@ package signoz
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/auditor/noopauditor"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/rulebasednotification"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
@@ -312,6 +314,12 @@ func NewGlobalProviderFactories(identNConfig identn.Config) factory.NamedMap[fac
|
||||
)
|
||||
}
|
||||
|
||||
func NewAuditorProviderFactories() factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
noopauditor.NewFactory(),
|
||||
)
|
||||
}
|
||||
|
||||
func NewFlaggerProviderFactories(registry featuretypes.Registry) factory.NamedMap[factory.ProviderFactory[flagger.FlaggerProvider, flagger.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
configflagger.NewFactory(registry),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/sqlroutingstore"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/apiserver"
|
||||
@@ -75,6 +76,7 @@ type SigNoz struct {
|
||||
QueryParser queryparser.QueryParser
|
||||
Flagger flagger.Flagger
|
||||
Gateway gateway.Gateway
|
||||
Auditor auditor.Auditor
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -94,6 +96,7 @@ func New(
|
||||
authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
|
||||
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
|
||||
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
|
||||
auditorProviderFactories func(licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]],
|
||||
querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler,
|
||||
) (*SigNoz, error) {
|
||||
// Initialize instrumentation
|
||||
@@ -371,6 +374,12 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize auditor from the variant-specific provider factories
|
||||
auditor, err := factory.NewProviderFromNamedMap(ctx, providerSettings, config.Auditor, auditorProviderFactories(licensing), config.Auditor.Provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize authns
|
||||
store := sqlauthnstore.NewStore(sqlstore)
|
||||
authNs, err := authNsCallback(ctx, providerSettings, store, licensing)
|
||||
@@ -470,6 +479,7 @@ func New(
|
||||
factory.NewNamedService(factory.MustNewName("tokenizer"), tokenizer),
|
||||
factory.NewNamedService(factory.MustNewName("authz"), authz),
|
||||
factory.NewNamedService(factory.MustNewName("user"), userService, factory.MustNewName("authz")),
|
||||
factory.NewNamedService(factory.MustNewName("auditor"), auditor),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -516,5 +526,6 @@ func New(
|
||||
QueryParser: queryParser,
|
||||
Flagger: flagger,
|
||||
Gateway: gateway,
|
||||
Auditor: auditor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user