mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-10 00:20:40 +01:00
Compare commits
2 Commits
nv/list-v2
...
worktree-j
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c4046d84a | ||
|
|
a91cee2b95 |
@@ -129,7 +129,7 @@ sqlstore:
|
||||
# The timeout for the sqlite database to wait for a lock.
|
||||
busy_timeout: 10s
|
||||
# The default transaction locking behavior. Supported values: deferred, immediate, exclusive.
|
||||
transaction_mode: deferred
|
||||
transaction_mode: immediate
|
||||
|
||||
##################### APIServer #####################
|
||||
apiserver:
|
||||
|
||||
@@ -3148,8 +3148,6 @@ components:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
legacy:
|
||||
type: boolean
|
||||
locked:
|
||||
type: boolean
|
||||
name:
|
||||
@@ -3182,7 +3180,6 @@ components:
|
||||
- name
|
||||
- tags
|
||||
- spec
|
||||
- legacy
|
||||
- pinned
|
||||
type: object
|
||||
DashboardtypesListedDashboardV2:
|
||||
@@ -3196,8 +3193,6 @@ components:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
legacy:
|
||||
type: boolean
|
||||
locked:
|
||||
type: boolean
|
||||
name:
|
||||
@@ -3228,7 +3223,6 @@ components:
|
||||
- name
|
||||
- tags
|
||||
- spec
|
||||
- legacy
|
||||
type: object
|
||||
DashboardtypesListedDashboardV2Spec:
|
||||
properties:
|
||||
|
||||
@@ -5005,10 +5005,6 @@ export interface DashboardtypesListedDashboardForUserV2DTO {
|
||||
* @type string
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
legacy: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -5084,10 +5080,6 @@ export interface DashboardtypesListedDashboardV2DTO {
|
||||
* @type string
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
legacy: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
|
||||
@@ -31,12 +31,6 @@ export enum LOCALSTORAGE {
|
||||
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
|
||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
QUICK_FILTERS_WIDTH_LOGS = 'QUICK_FILTERS_WIDTH_LOGS',
|
||||
QUICK_FILTERS_WIDTH_TRACES = 'QUICK_FILTERS_WIDTH_TRACES',
|
||||
QUICK_FILTERS_WIDTH_METER = 'QUICK_FILTERS_WIDTH_METER',
|
||||
QUICK_FILTERS_WIDTH_API_MONITORING = 'QUICK_FILTERS_WIDTH_API_MONITORING',
|
||||
QUICK_FILTERS_WIDTH_EXCEPTIONS = 'QUICK_FILTERS_WIDTH_EXCEPTIONS',
|
||||
QUICK_FILTERS_WIDTH_INFRA = 'QUICK_FILTERS_WIDTH_INFRA',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
SPAN_DETAILS_PINNED_ATTRIBUTES = 'SPAN_DETAILS_PINNED_ATTRIBUTES',
|
||||
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
|
||||
|
||||
@@ -163,23 +163,12 @@
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
// Width is owned by ResizableBox (inline style); this section is the
|
||||
// ResizableBox root, so it stays position: relative for the drag handle.
|
||||
.api-quick-filter-left-section {
|
||||
.resizable-box__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.quick-filters-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,49 +4,21 @@ import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
|
||||
import DomainList from './Domains/DomainList';
|
||||
|
||||
import './Explorer.styles.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 260;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function Explorer(): JSX.Element {
|
||||
useEffect(() => {
|
||||
logEvent('API Monitoring: Landing page visited', {});
|
||||
}, []);
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_API_MONITORING,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className="api-quick-filter-left-section"
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<section className="api-quick-filter-left-section">
|
||||
<QuickFilters
|
||||
className="qf-api-monitoring"
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
@@ -55,7 +27,7 @@ function Explorer(): JSX.Element {
|
||||
showQueryName={false}
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
/>
|
||||
</ResizableBox>
|
||||
</section>
|
||||
<DomainList />
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
|
||||
@@ -6,7 +6,6 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import K8sBaseDetails from 'container/InfraMonitoringK8s/Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from 'container/InfraMonitoringK8s/Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from 'container/InfraMonitoringK8s/Base/types';
|
||||
@@ -17,8 +16,6 @@ import {
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -43,21 +40,8 @@ import { getHostsQuickFiltersConfig } from './utils';
|
||||
import styles from './InfraMonitoringHosts.module.scss';
|
||||
import { ArrowUpToLine, Filter } from '@signozhq/icons';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function Hosts(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
|
||||
@@ -155,18 +139,7 @@ function Hosts(): JSX.Element {
|
||||
<div className={styles.infraMonitoringContainer}>
|
||||
<div className={styles.infraContentRow}>
|
||||
{showFilters && (
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className={styles.quickFiltersContainer}
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<div className={styles.quickFiltersContainer}>
|
||||
<div className={styles.quickFiltersContainerHeader}>
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
<Tooltip title="Collapse Filters">
|
||||
@@ -183,7 +156,7 @@ function Hosts(): JSX.Element {
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleQuickFiltersChange}
|
||||
/>
|
||||
</ResizableBox>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`${styles.listContainer}${
|
||||
|
||||
@@ -44,20 +44,26 @@
|
||||
}
|
||||
|
||||
.quickFiltersContainer {
|
||||
// Width is owned by ResizableBox (inline style). No overflow here: the panel
|
||||
// must not scroll (QuickFilters scrolls its own list internally) — an
|
||||
// overflow on the root would also clip the edge-mounted resize grip.
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
overflow-y: auto;
|
||||
|
||||
:global(.resizable-box__content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
:global(.quick-filters-container) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
}
|
||||
|
||||
:global(.ant-collapse-header) {
|
||||
@@ -136,9 +142,7 @@
|
||||
}
|
||||
|
||||
.listContainerFiltersVisible {
|
||||
// Width is driven by flex now that the filters panel is resizable; the list
|
||||
// fills whatever space the (variable-width) panel leaves.
|
||||
max-width: 100%;
|
||||
max-width: calc(100% - 280px);
|
||||
}
|
||||
|
||||
.quickFiltersToggleContainer {
|
||||
|
||||
@@ -6,7 +6,6 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import K8sBaseDetails from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from 'container/InfraMonitoringK8sV2/Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
|
||||
@@ -17,8 +16,6 @@ import {
|
||||
} from 'container/InfraMonitoringK8sV2/hooks';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -43,21 +40,8 @@ import { getHostsQuickFiltersConfig } from './utils';
|
||||
import styles from './InfraMonitoringHosts.module.scss';
|
||||
import { ArrowUpToLine, Filter } from '@signozhq/icons';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function Hosts(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
|
||||
@@ -155,18 +139,7 @@ function Hosts(): JSX.Element {
|
||||
<div className={styles.infraMonitoringContainer}>
|
||||
<div className={styles.infraContentRow}>
|
||||
{showFilters && (
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className={styles.quickFiltersContainer}
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<div className={styles.quickFiltersContainer}>
|
||||
<div className={styles.quickFiltersContainerHeader}>
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
<Tooltip title="Collapse Filters">
|
||||
@@ -183,7 +156,7 @@ function Hosts(): JSX.Element {
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleQuickFiltersChange}
|
||||
/>
|
||||
</ResizableBox>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`${styles.listContainer}${
|
||||
|
||||
@@ -44,20 +44,26 @@
|
||||
}
|
||||
|
||||
.quickFiltersContainer {
|
||||
// Width is owned by ResizableBox (inline style). No overflow here: the panel
|
||||
// must not scroll (QuickFilters scrolls its own list internally) — an
|
||||
// overflow on the root would also clip the edge-mounted resize grip.
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
overflow-y: auto;
|
||||
|
||||
:global(.resizable-box__content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
:global(.quick-filters-container) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
}
|
||||
|
||||
:global(.ant-collapse-header) {
|
||||
@@ -136,9 +142,7 @@
|
||||
}
|
||||
|
||||
.listContainerFiltersVisible {
|
||||
// Width is driven by flex now that the filters panel is resizable; the list
|
||||
// fills whatever space the (variable-width) panel leaves.
|
||||
max-width: 100%;
|
||||
max-width: calc(100% - 280px);
|
||||
}
|
||||
|
||||
.quickFiltersToggleContainer {
|
||||
|
||||
@@ -44,15 +44,26 @@
|
||||
}
|
||||
|
||||
.quickFiltersContainer {
|
||||
// Width is owned by ResizableBox (inline style). No overflow here: the panel
|
||||
// must not scroll (QuickFilters scrolls its own list internally) — an
|
||||
// overflow on the root would also clip the edge-mounted resize grip.
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
overflow-y: auto;
|
||||
|
||||
:global(.resizable-box__content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
}
|
||||
|
||||
:global(.quick-filters) {
|
||||
@@ -117,9 +128,7 @@
|
||||
}
|
||||
|
||||
.listContainerFiltersVisible {
|
||||
// Width is driven by flex now that the filters panel is resizable; the list
|
||||
// fills whatever space the (variable-width) panel leaves.
|
||||
max-width: 100%;
|
||||
max-width: calc(100% - 280px);
|
||||
}
|
||||
|
||||
.categorySelectorSection {
|
||||
@@ -210,16 +219,8 @@
|
||||
|
||||
.quickFiltersSection {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
:global(.quick-filters-container) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import logEvent from 'api/common/logEvent';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import {
|
||||
@@ -23,8 +22,6 @@ import {
|
||||
Workflow,
|
||||
} from '@signozhq/icons';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
@@ -59,23 +56,9 @@ import K8sVolumesList from './Volumes/K8sVolumesList';
|
||||
|
||||
import styles from './InfraMonitoringK8s.module.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
export default function InfraMonitoringK8s(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
const [, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
@@ -229,18 +212,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
<div className={styles.infraMonitoringContainer}>
|
||||
<div className={styles.infraContentRow}>
|
||||
{showFilters && (
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className={styles.quickFiltersContainer}
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<div className={styles.quickFiltersContainer}>
|
||||
<div className={styles.categorySelectorSection}>
|
||||
<div className={styles.sectionHeader} data-type="resource">
|
||||
<Typography.Text className={styles.sectionLabel}>
|
||||
@@ -293,7 +265,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ResizableBox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
@@ -44,15 +44,26 @@
|
||||
}
|
||||
|
||||
.quickFiltersContainer {
|
||||
// Width is owned by ResizableBox (inline style). No overflow here: the panel
|
||||
// must not scroll (QuickFilters scrolls its own list internally) — an
|
||||
// overflow on the root would also clip the edge-mounted resize grip.
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
overflow-y: auto;
|
||||
|
||||
:global(.resizable-box__content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
}
|
||||
|
||||
:global(.quick-filters) {
|
||||
@@ -117,9 +128,7 @@
|
||||
}
|
||||
|
||||
.listContainerFiltersVisible {
|
||||
// Width is driven by flex now that the filters panel is resizable; the list
|
||||
// fills whatever space the (variable-width) panel leaves.
|
||||
max-width: 100%;
|
||||
max-width: calc(100% - 280px);
|
||||
}
|
||||
|
||||
.categorySelectorSection {
|
||||
@@ -210,16 +219,8 @@
|
||||
|
||||
.quickFiltersSection {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
:global(.quick-filters-container) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import logEvent from 'api/common/logEvent';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import {
|
||||
@@ -23,8 +22,6 @@ import {
|
||||
Workflow,
|
||||
} from '@signozhq/icons';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
@@ -59,23 +56,9 @@ import K8sVolumesList from './Volumes/K8sVolumesList';
|
||||
|
||||
import styles from './InfraMonitoringK8s.module.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
export default function InfraMonitoringK8s(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
const [, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
@@ -229,18 +212,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
<div className={styles.infraMonitoringContainer}>
|
||||
<div className={styles.infraContentRow}>
|
||||
{showFilters && (
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className={styles.quickFiltersContainer}
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<div className={styles.quickFiltersContainer}>
|
||||
<div className={styles.categorySelectorSection}>
|
||||
<div className={styles.sectionHeader} data-type="resource">
|
||||
<Typography.Text className={styles.sectionLabel}>
|
||||
@@ -293,7 +265,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ResizableBox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
@@ -3,23 +3,12 @@
|
||||
flex-direction: row;
|
||||
|
||||
.meter-explorer-quick-filters-section {
|
||||
// Width is owned by ResizableBox (inline style).
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.resizable-box__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.quick-filters-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.meter-explorer-content-section {
|
||||
@@ -97,9 +86,7 @@
|
||||
|
||||
&.quick-filters-open {
|
||||
.meter-explorer-content-section {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
width: calc(100% - 280px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import cx from 'classnames';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
@@ -19,8 +18,6 @@ import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Filter } from '@signozhq/icons';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -33,10 +30,6 @@ import { splitQueryIntoOneChartPerQuery } from './utils';
|
||||
|
||||
import './Explorer.styles.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function Explorer(): JSX.Element {
|
||||
const {
|
||||
handleRunQuery,
|
||||
@@ -62,16 +55,6 @@ function Explorer(): JSX.Element {
|
||||
|
||||
const [showQuickFilters, setShowQuickFilters] = useState(true);
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_METER,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
@@ -144,19 +127,10 @@ function Explorer(): JSX.Element {
|
||||
'quick-filters-open': showQuickFilters,
|
||||
})}
|
||||
>
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
<div
|
||||
className={cx('meter-explorer-quick-filters-section', {
|
||||
hidden: !showQuickFilters,
|
||||
})}
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<QuickFilters
|
||||
className="qf-meter-explorer"
|
||||
@@ -168,7 +142,7 @@ function Explorer(): JSX.Element {
|
||||
setShowQuickFilters(!showQuickFilters);
|
||||
}}
|
||||
/>
|
||||
</ResizableBox>
|
||||
</div>
|
||||
|
||||
<div className="meter-explorer-content-section">
|
||||
<div className="meter-explorer-explore-content">
|
||||
|
||||
@@ -20,23 +20,12 @@
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
// Width is owned by ResizableBox (inline style); this section is the
|
||||
// ResizableBox root, so it stays position: relative for the drag handle.
|
||||
.all-errors-quick-filter-section {
|
||||
.resizable-box__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.quick-filters-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.all-errors-right-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,18 +18,12 @@ import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import history from 'lib/history';
|
||||
import { isNull } from 'lodash-es';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
|
||||
import { routes } from './config';
|
||||
import { useAllErrorsQueryState } from './QueryStateContext';
|
||||
|
||||
import './AllErrors.styles.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 260;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function AllErrors(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
@@ -61,38 +55,17 @@ function AllErrors(): JSX.Element {
|
||||
setShowFilters((prev) => !prev);
|
||||
};
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_EXCEPTIONS,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cx('all-errors-page', showFilters ? 'filter-visible' : '')}>
|
||||
{showFilters && (
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className="all-errors-quick-filter-section"
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<section className={cx('all-errors-quick-filter-section')}>
|
||||
<QuickFilters
|
||||
className="qf-exceptions"
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
signal={SignalType.EXCEPTIONS}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
/>
|
||||
</ResizableBox>
|
||||
</section>
|
||||
)}
|
||||
<section
|
||||
className={cx(
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import ActionsPopover from './ActionsPopover';
|
||||
|
||||
const baseProps = {
|
||||
link: '/dashboard/abc',
|
||||
dashboardId: 'abc',
|
||||
dashboardName: 'My Dashboard',
|
||||
createdBy: 'someone-else@signoz.io',
|
||||
isLocked: false,
|
||||
tags: [],
|
||||
canEdit: true,
|
||||
onView: jest.fn(),
|
||||
};
|
||||
|
||||
describe('ActionsPopover', () => {
|
||||
it('shows the full set of actions for a v2 dashboard', async () => {
|
||||
render(<ActionsPopover {...baseProps} />);
|
||||
await userEvent.click(screen.getByTestId('dashboard-action-icon'));
|
||||
|
||||
await screen.findByTestId('dashboard-action-view');
|
||||
expect(screen.getByTestId('dashboard-action-view')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('dashboard-action-open-new-tab'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-action-copy-link')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-action-rename')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-action-edit-tags')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-action-duplicate')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-action-delete')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps only Delete for a legacy dashboard', async () => {
|
||||
render(<ActionsPopover {...baseProps} isLegacy />);
|
||||
await userEvent.click(screen.getByTestId('dashboard-action-icon'));
|
||||
|
||||
// Delete is the one action that still applies to a legacy blob.
|
||||
await screen.findByTestId('dashboard-action-delete');
|
||||
expect(screen.getByTestId('dashboard-action-delete')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId('dashboard-action-view')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('dashboard-action-open-new-tab'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('dashboard-action-copy-link'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('dashboard-action-rename'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('dashboard-action-edit-tags'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('dashboard-action-duplicate'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('dashboard-action-lock')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -46,10 +46,6 @@ interface Props {
|
||||
// Edit permission (edit_dashboard). Read actions show regardless; edit actions are hidden without it.
|
||||
canEdit: boolean;
|
||||
onView: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
// A legacy (pre-v2) dashboard has no v2 spec, so the actions that operate on
|
||||
// one (view, open, copy link, rename, edit tags, duplicate, lock) don't apply —
|
||||
// only Delete is kept.
|
||||
isLegacy?: boolean;
|
||||
}
|
||||
|
||||
function ActionsPopover({
|
||||
@@ -61,7 +57,6 @@ function ActionsPopover({
|
||||
tags,
|
||||
canEdit,
|
||||
onView,
|
||||
isLegacy = false,
|
||||
}: Props): JSX.Element {
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@@ -114,132 +109,128 @@ function ActionsPopover({
|
||||
// row's onClick, which would navigate to the dashboard.
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- wrapper only guards propagation, not an interactive control
|
||||
<div className={styles.content} onClick={(e): void => e.stopPropagation()}>
|
||||
{!isLegacy && (
|
||||
<>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Expand size={14} />}
|
||||
onClick={onView}
|
||||
testId="dashboard-action-view"
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<SquareArrowOutUpRight size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openInNewTab(link);
|
||||
}}
|
||||
testId="dashboard-action-open-new-tab"
|
||||
>
|
||||
Open in New Tab
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Link2 size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setCopy(getAbsoluteUrl(link));
|
||||
}}
|
||||
testId="dashboard-action-copy-link"
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={
|
||||
isLocked ? 'This dashboard is locked, so it cannot be renamed.' : ''
|
||||
}
|
||||
>
|
||||
<span className={styles.menuItemWrap}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<PenLine size={14} />}
|
||||
disabled={isLocked}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!isLocked) {
|
||||
setIsRenameOpen(true);
|
||||
}
|
||||
}}
|
||||
testId="dashboard-action-rename"
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={
|
||||
isLocked
|
||||
? 'This dashboard is locked, so its tags cannot be edited.'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<span className={styles.menuItemWrap}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Tag size={14} />}
|
||||
disabled={isLocked}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!isLocked) {
|
||||
setIsEditTagsOpen(true);
|
||||
}
|
||||
}}
|
||||
testId="dashboard-action-edit-tags"
|
||||
>
|
||||
{tags.length > 0 ? 'Edit Tags' : 'Add Tags'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Expand size={14} />}
|
||||
onClick={onView}
|
||||
testId="dashboard-action-view"
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<SquareArrowOutUpRight size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openInNewTab(link);
|
||||
}}
|
||||
testId="dashboard-action-open-new-tab"
|
||||
>
|
||||
Open in New Tab
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Link2 size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setCopy(getAbsoluteUrl(link));
|
||||
}}
|
||||
testId="dashboard-action-copy-link"
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={
|
||||
isLocked ? 'This dashboard is locked, so it cannot be renamed.' : ''
|
||||
}
|
||||
>
|
||||
<span className={styles.menuItemWrap}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Copy size={14} />}
|
||||
loading={isCloning}
|
||||
prefix={<PenLine size={14} />}
|
||||
disabled={isLocked}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
runClone();
|
||||
if (!isLocked) {
|
||||
setIsRenameOpen(true);
|
||||
}
|
||||
}}
|
||||
testId="dashboard-action-duplicate"
|
||||
testId="dashboard-action-rename"
|
||||
>
|
||||
Duplicate
|
||||
Rename
|
||||
</Button>
|
||||
)}
|
||||
{canToggleLock && (
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={
|
||||
isLocked
|
||||
? 'This dashboard is locked, so its tags cannot be edited.'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<span className={styles.menuItemWrap}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<LockKeyhole size={14} />}
|
||||
loading={isTogglingLock}
|
||||
prefix={<Tag size={14} />}
|
||||
disabled={isLocked}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
runLockToggle();
|
||||
if (!isLocked) {
|
||||
setIsEditTagsOpen(true);
|
||||
}
|
||||
}}
|
||||
testId="dashboard-action-lock"
|
||||
testId="dashboard-action-edit-tags"
|
||||
>
|
||||
{isLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
{tags.length > 0 ? 'Edit Tags' : 'Add Tags'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Copy size={14} />}
|
||||
loading={isCloning}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
runClone();
|
||||
}}
|
||||
testId="dashboard-action-duplicate"
|
||||
>
|
||||
Duplicate
|
||||
</Button>
|
||||
)}
|
||||
{canToggleLock && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<LockKeyhole size={14} />}
|
||||
loading={isTogglingLock}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
runLockToggle();
|
||||
}}
|
||||
testId="dashboard-action-lock"
|
||||
>
|
||||
{isLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && (
|
||||
<DeleteActionItem
|
||||
@@ -247,7 +238,6 @@ function ActionsPopover({
|
||||
dashboardName={dashboardName}
|
||||
createdBy={createdBy}
|
||||
isLocked={isLocked}
|
||||
showDivider={!isLegacy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -23,9 +23,6 @@ interface Props {
|
||||
dashboardName: string;
|
||||
createdBy: string;
|
||||
isLocked: boolean;
|
||||
// Delete sits below the other actions, so it leads with a divider. When it's
|
||||
// the only item (a legacy dashboard), the divider is suppressed.
|
||||
showDivider?: boolean;
|
||||
}
|
||||
|
||||
function DeleteActionItem({
|
||||
@@ -33,7 +30,6 @@ function DeleteActionItem({
|
||||
dashboardName,
|
||||
createdBy,
|
||||
isLocked,
|
||||
showDivider = true,
|
||||
}: Props): JSX.Element {
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
const { user } = useAppContext();
|
||||
@@ -104,7 +100,7 @@ function DeleteActionItem({
|
||||
|
||||
return (
|
||||
<>
|
||||
{showDivider && <Divider />}
|
||||
<Divider />
|
||||
<Tooltip placement="left" title={tooltip}>
|
||||
<span className={styles.menuItemWrap}>
|
||||
<Button
|
||||
|
||||
@@ -55,10 +55,6 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.legacyBadge {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.tagsWithActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -75,17 +71,6 @@
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
/* Wrap so the tooltip has a hoverable target even when the pin button is disabled
|
||||
(a legacy dashboard can't be pinned). */
|
||||
.pinButtonWrap {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.pinButtonWrap button:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pinButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import type { DashboardListItem } from '../../utils/helpers';
|
||||
|
||||
import DashboardRow from './DashboardRow';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockTogglePin = jest.fn();
|
||||
jest.mock('../../hooks/usePinDashboard', () => ({
|
||||
usePinDashboard: (): { togglePin: jest.Mock; isUpdating: boolean } => ({
|
||||
togglePin: mockTogglePin,
|
||||
isUpdating: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/common/logEvent', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
// Stub the actions menu so this suite stays focused on the row; the isLegacy
|
||||
// wiring is asserted via the recorded prop, and the menu itself is covered by
|
||||
// ActionsPopover's own tests.
|
||||
jest.mock('../ActionsPopover/ActionsPopover', () => ({
|
||||
__esModule: true,
|
||||
default: ({ isLegacy }: { isLegacy?: boolean }): JSX.Element => (
|
||||
<div data-testid="actions-popover" data-legacy={String(!!isLegacy)} />
|
||||
),
|
||||
}));
|
||||
|
||||
const makeDashboard = (
|
||||
overrides: Partial<DashboardListItem> = {},
|
||||
): DashboardListItem =>
|
||||
({
|
||||
id: 'dash-1',
|
||||
legacy: false,
|
||||
pinned: false,
|
||||
locked: false,
|
||||
image: '',
|
||||
createdBy: 'alice@signoz.io',
|
||||
updatedBy: 'alice@signoz.io',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-02T00:00:00Z',
|
||||
tags: [],
|
||||
spec: { display: { name: 'My Dashboard' } },
|
||||
...overrides,
|
||||
}) as unknown as DashboardListItem;
|
||||
|
||||
const renderRow = (dashboard: DashboardListItem): void => {
|
||||
render(
|
||||
<DashboardRow
|
||||
dashboard={dashboard}
|
||||
index={0}
|
||||
canEdit
|
||||
showUpdatedAt={false}
|
||||
showUpdatedBy={false}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('DashboardRow', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('a v2 dashboard', () => {
|
||||
it('navigates on click and shows no legacy badge', async () => {
|
||||
renderRow(makeDashboard());
|
||||
|
||||
expect(screen.queryByTestId('dashboard-legacy-0')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-pin-0')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('actions-popover')).toHaveAttribute(
|
||||
'data-legacy',
|
||||
'false',
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('dashboard-title-0'));
|
||||
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByTestId('legacy-dashboard-id')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('a legacy dashboard', () => {
|
||||
it('badges the row, disables pin and gates the actions menu', () => {
|
||||
renderRow(makeDashboard({ legacy: true }));
|
||||
|
||||
expect(screen.getByTestId('dashboard-legacy-0')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-pin-0')).toBeDisabled();
|
||||
expect(screen.getByTestId('actions-popover')).toHaveAttribute(
|
||||
'data-legacy',
|
||||
'true',
|
||||
);
|
||||
});
|
||||
|
||||
it('opens the explanatory dialog instead of navigating', async () => {
|
||||
renderRow(makeDashboard({ legacy: true }));
|
||||
|
||||
await userEvent.click(screen.getByTestId('dashboard-title-0'));
|
||||
|
||||
expect(mockSafeNavigate).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('legacy-dashboard-id')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import TagBadge from 'components/TagBadge/TagBadge';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
@@ -20,7 +18,6 @@ import { useDashboardViewsStore } from '../../store/useDashboardViewsStore';
|
||||
import type { DashboardListItem } from '../../utils/helpers';
|
||||
import { lastUpdatedLabel, tagsToStrings } from '../../utils/helpers';
|
||||
import ActionsPopover from '../ActionsPopover/ActionsPopover';
|
||||
import LegacyDashboardDialog from '../LegacyDashboardDialog/LegacyDashboardDialog';
|
||||
|
||||
import styles from './DashboardRow.module.scss';
|
||||
|
||||
@@ -45,11 +42,6 @@ function DashboardRow({
|
||||
const markViewed = useDashboardViewsStore((s) => s.markViewed);
|
||||
const { togglePin, isUpdating } = usePinDashboard();
|
||||
|
||||
const [isLegacyDialogOpen, setIsLegacyDialogOpen] = useState(false);
|
||||
|
||||
// A legacy dashboard is stored in the pre-v2 shape and has no v2 spec, so it
|
||||
// can't be opened in the new experience — clicking it explains this instead.
|
||||
const isLegacy = !!dashboard.legacy;
|
||||
const isPinned = !!dashboard.pinned;
|
||||
const id = dashboard.id;
|
||||
const name = dashboard.spec?.display?.name ?? '';
|
||||
@@ -75,17 +67,9 @@ function DashboardRow({
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
if (isLegacy) {
|
||||
setIsLegacyDialogOpen(true);
|
||||
void logEvent('Dashboard List: Clicked on legacy dashboard', {
|
||||
dashboardId: id,
|
||||
dashboardName: name,
|
||||
});
|
||||
return;
|
||||
}
|
||||
markViewed(id);
|
||||
safeNavigate(link, { newTab: isModifierKeyPressed(event) });
|
||||
void logEvent('Dashboard List: Clicked on dashboard', {
|
||||
logEvent('Dashboard List: Clicked on dashboard', {
|
||||
dashboardId: id,
|
||||
dashboardName: name,
|
||||
});
|
||||
@@ -93,17 +77,9 @@ function DashboardRow({
|
||||
|
||||
const onTogglePin = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
event.stopPropagation();
|
||||
if (isLegacy) {
|
||||
return;
|
||||
}
|
||||
togglePin(id, isPinned);
|
||||
};
|
||||
|
||||
const pinLabel = isPinned ? 'Unpin dashboard' : 'Pin dashboard';
|
||||
const pinTooltip = isLegacy
|
||||
? "This dashboard isn't available in the new experience, so it can't be pinned"
|
||||
: pinLabel;
|
||||
|
||||
// Only long titles are truncated, so only they need the full-name tooltip;
|
||||
// wrapping conditionally avoids an empty hanging tooltip for short names.
|
||||
const titleLink = (
|
||||
@@ -119,144 +95,120 @@ function DashboardRow({
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.row} onClick={onClickHandler}>
|
||||
<div className={styles.titleWithAction}>
|
||||
<div className={styles.titleBlock}>
|
||||
{name.length > 50 ? (
|
||||
<TooltipSimple title={name} side="bottom" disableHoverableContent>
|
||||
{titleLink}
|
||||
</TooltipSimple>
|
||||
) : (
|
||||
titleLink
|
||||
)}
|
||||
{isLegacy && (
|
||||
<Badge
|
||||
color="amber"
|
||||
variant="outline"
|
||||
className={styles.legacyBadge}
|
||||
testId={`dashboard-legacy-${index}`}
|
||||
>
|
||||
Legacy
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.tagsWithActions}>
|
||||
{tags.length > 0 && (
|
||||
<div className={styles.tags}>
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<TagBadge key={tag}>{tag}</TagBadge>
|
||||
))}
|
||||
{tags.length > 3 && (
|
||||
<TagBadge key={tags[3]}>+{tags.length - 3}</TagBadge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLocked && (
|
||||
<TooltipSimple
|
||||
title="This dashboard is locked"
|
||||
side="top"
|
||||
disableHoverableContent
|
||||
>
|
||||
<span
|
||||
className={styles.lockIcon}
|
||||
data-testid={`dashboard-lock-${index}`}
|
||||
>
|
||||
<LockKeyhole size={14} />
|
||||
</span>
|
||||
<div className={styles.row} onClick={onClickHandler}>
|
||||
<div className={styles.titleWithAction}>
|
||||
<div className={styles.titleBlock}>
|
||||
{name.length > 50 ? (
|
||||
<TooltipSimple title={name} side="bottom" disableHoverableContent>
|
||||
{titleLink}
|
||||
</TooltipSimple>
|
||||
) : (
|
||||
titleLink
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TooltipSimple title={pinTooltip} side="top" disableHoverableContent>
|
||||
<span className={styles.pinButtonWrap}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={cx(styles.pinButton, {
|
||||
[styles.pinButtonOn]: isPinned && !isLegacy,
|
||||
})}
|
||||
aria-label={pinLabel}
|
||||
data-testid={`dashboard-pin-${index}`}
|
||||
disabled={isUpdating || isLegacy}
|
||||
onClick={onTogglePin}
|
||||
>
|
||||
{isPinned ? (
|
||||
<>
|
||||
<Pin size={14} className={styles.pinnedIcon} />
|
||||
<PinOff size={14} className={styles.unpinIcon} />
|
||||
</>
|
||||
) : (
|
||||
<Pin size={14} />
|
||||
)}
|
||||
</Button>
|
||||
<div className={styles.tagsWithActions}>
|
||||
{tags.length > 0 && (
|
||||
<div className={styles.tags}>
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<TagBadge key={tag}>{tag}</TagBadge>
|
||||
))}
|
||||
{tags.length > 3 && (
|
||||
<TagBadge key={tags[3]}>+{tags.length - 3}</TagBadge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLocked && (
|
||||
<TooltipSimple
|
||||
title="This dashboard is locked"
|
||||
side="top"
|
||||
disableHoverableContent
|
||||
>
|
||||
<span className={styles.lockIcon} data-testid={`dashboard-lock-${index}`}>
|
||||
<LockKeyhole size={14} />
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
|
||||
<ActionsPopover
|
||||
link={link}
|
||||
dashboardId={id}
|
||||
dashboardName={name}
|
||||
createdBy={createdBy}
|
||||
isLocked={isLocked}
|
||||
tags={tags}
|
||||
canEdit={canEdit}
|
||||
onView={onClickHandler}
|
||||
isLegacy={isLegacy}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{formattedCreatedAt}</Typography.Text>
|
||||
</div>
|
||||
<TooltipSimple
|
||||
title={isPinned ? 'Unpin dashboard' : 'Pin dashboard'}
|
||||
side="top"
|
||||
disableHoverableContent
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={cx(styles.pinButton, { [styles.pinButtonOn]: isPinned })}
|
||||
aria-label={isPinned ? 'Unpin dashboard' : 'Pin dashboard'}
|
||||
data-testid={`dashboard-pin-${index}`}
|
||||
disabled={isUpdating}
|
||||
onClick={onTogglePin}
|
||||
>
|
||||
{isPinned ? (
|
||||
<>
|
||||
<Pin size={14} className={styles.pinnedIcon} />
|
||||
<PinOff size={14} className={styles.unpinIcon} />
|
||||
</>
|
||||
) : (
|
||||
<Pin size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
{createdBy && (
|
||||
<div className={styles.createdBy}>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{createdBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{createdBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showUpdatedAt && (
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{lastUpdatedLabel(updatedAt)}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatedBy && showUpdatedBy && (
|
||||
<div className={styles.updatedBy}>
|
||||
<Typography.Text className={styles.byLabel}>
|
||||
Last Updated By -
|
||||
</Typography.Text>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{updatedBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{updatedBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isLegacy && (
|
||||
<LegacyDashboardDialog
|
||||
open={isLegacyDialogOpen}
|
||||
<ActionsPopover
|
||||
link={link}
|
||||
dashboardId={id}
|
||||
dashboardName={name}
|
||||
onClose={(): void => setIsLegacyDialogOpen(false)}
|
||||
createdBy={createdBy}
|
||||
isLocked={isLocked}
|
||||
tags={tags}
|
||||
canEdit={canEdit}
|
||||
onView={onClickHandler}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{formattedCreatedAt}</Typography.Text>
|
||||
</div>
|
||||
|
||||
{createdBy && (
|
||||
<div className={styles.createdBy}>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{createdBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{createdBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showUpdatedAt && (
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{lastUpdatedLabel(updatedAt)}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatedBy && showUpdatedBy && (
|
||||
<div className={styles.updatedBy}>
|
||||
<Typography.Text className={styles.byLabel}>
|
||||
Last Updated By -
|
||||
</Typography.Text>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{updatedBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{updatedBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
|
||||
strong {
|
||||
color: var(--l1-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.idField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.idLabel {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.idRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 6px 6px 12px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.idValue {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import LegacyDashboardDialog from './LegacyDashboardDialog';
|
||||
|
||||
const mockCopy = jest.fn();
|
||||
jest.mock('react-use', () => ({
|
||||
...jest.requireActual('react-use'),
|
||||
useCopyToClipboard: (): [unknown, (value: string) => void] => [{}, mockCopy],
|
||||
}));
|
||||
|
||||
const mockToastSuccess = jest.fn();
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { success: (message: string): void => mockToastSuccess(message) },
|
||||
}));
|
||||
|
||||
const mockContactSupport = jest.fn();
|
||||
jest.mock('container/Integrations/utils', () => ({
|
||||
handleContactSupport: (isCloud: boolean): void => mockContactSupport(isCloud),
|
||||
}));
|
||||
|
||||
const DASHBOARD_ID = '0f9a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c';
|
||||
|
||||
describe('LegacyDashboardDialog', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const setup = (open = true): void => {
|
||||
render(
|
||||
<LegacyDashboardDialog
|
||||
open={open}
|
||||
dashboardId={DASHBOARD_ID}
|
||||
dashboardName="My Legacy Dashboard"
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
it('surfaces the dashboard name and id', () => {
|
||||
setup();
|
||||
expect(screen.getByText('My Legacy Dashboard')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('legacy-dashboard-id')).toHaveTextContent(
|
||||
DASHBOARD_ID,
|
||||
);
|
||||
});
|
||||
|
||||
it('copies the dashboard id and confirms with a toast', async () => {
|
||||
setup();
|
||||
await userEvent.click(screen.getByTestId('legacy-dashboard-copy-id'));
|
||||
expect(mockCopy).toHaveBeenCalledWith(DASHBOARD_ID);
|
||||
expect(mockToastSuccess).toHaveBeenCalledWith('Dashboard ID copied');
|
||||
});
|
||||
|
||||
it('routes the user to support', async () => {
|
||||
setup();
|
||||
await userEvent.click(screen.getByTestId('legacy-dashboard-contact-support'));
|
||||
expect(mockContactSupport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders nothing when closed', () => {
|
||||
setup(false);
|
||||
expect(screen.queryByTestId('legacy-dashboard-id')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ArrowUpRight, Copy } from '@signozhq/icons';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
|
||||
import styles from './LegacyDashboardDialog.module.scss';
|
||||
|
||||
interface LegacyDashboardDialogProps {
|
||||
open: boolean;
|
||||
dashboardId: string;
|
||||
dashboardName: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explains why a legacy (pre-v2) dashboard can't be opened in the new experience
|
||||
* and hands the user the dashboard ID to share with support. Legacy rows are
|
||||
* surfaced by the list API with `legacy: true` but have no v2 spec to render.
|
||||
*/
|
||||
function LegacyDashboardDialog({
|
||||
open,
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
onClose,
|
||||
}: LegacyDashboardDialogProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const { isCloudUser } = useGetTenantLicense();
|
||||
|
||||
const onCopyId = (): void => {
|
||||
copyToClipboard(dashboardId);
|
||||
toast.success('Dashboard ID copied');
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
title="This dashboard isn't available in the new experience"
|
||||
open={open}
|
||||
width="narrow"
|
||||
onOpenChange={(next): void => {
|
||||
if (!next) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
footer={
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="md"
|
||||
onClick={onClose}
|
||||
testId="legacy-dashboard-close"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="md"
|
||||
suffix={<ArrowUpRight size={14} />}
|
||||
onClick={(): void => handleContactSupport(!!isCloudUser)}
|
||||
testId="legacy-dashboard-contact-support"
|
||||
>
|
||||
Contact Support
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.body}>
|
||||
<Typography.Text className={styles.description}>
|
||||
<strong>{dashboardName || 'This dashboard'}</strong> hasn't been
|
||||
migrated to the new dashboard experience yet, so it can't be opened
|
||||
here. Share the dashboard ID below with support and we'll help you
|
||||
move it over.
|
||||
</Typography.Text>
|
||||
|
||||
<div className={styles.idField}>
|
||||
<Typography.Text className={styles.idLabel}>Dashboard ID</Typography.Text>
|
||||
<div className={styles.idRow}>
|
||||
<Typography.Text
|
||||
className={styles.idValue}
|
||||
data-testid="legacy-dashboard-id"
|
||||
>
|
||||
{dashboardId}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
prefix={<Copy size={14} />}
|
||||
aria-label="Copy dashboard ID"
|
||||
onClick={onCopyId}
|
||||
testId="legacy-dashboard-copy-id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default LegacyDashboardDialog;
|
||||
@@ -41,18 +41,15 @@
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
// Width is owned by ResizableBox (inline style); this section is the
|
||||
// ResizableBox root, so it stays position: relative for the drag handle.
|
||||
.log-quick-filter-left-section {
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.resizable-box__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.quick-filters-container {
|
||||
flex: 1;
|
||||
@@ -61,9 +58,7 @@
|
||||
}
|
||||
|
||||
.log-module-right-section {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ import {
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { defaultTo, isEmpty, isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { EventSourceProvider } from 'providers/EventSource';
|
||||
import { Warning } from 'types/api';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -46,23 +44,9 @@ import { ExplorerViews } from './utils';
|
||||
|
||||
import './LogsExplorer.styles.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 260;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function LogsExplorer(): JSX.Element {
|
||||
const [showLiveLogs, setShowLiveLogs] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_LOGS,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
// Get panel type from URL
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
@@ -242,25 +226,14 @@ function LogsExplorer(): JSX.Element {
|
||||
className={cx('logs-module-page', showFilters ? 'filter-visible' : '')}
|
||||
>
|
||||
{showFilters && (
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className="log-quick-filter-left-section"
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<section className={cx('log-quick-filter-left-section')}>
|
||||
<QuickFilters
|
||||
className="qf-logs-explorer"
|
||||
signal={SignalType.LOGS}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
/>
|
||||
</ResizableBox>
|
||||
</section>
|
||||
)}
|
||||
<section className={cx('log-module-right-section')}>
|
||||
<Toolbar
|
||||
|
||||
@@ -76,8 +76,7 @@
|
||||
--input-focus-border-color: var(--internal-ant-border-color-hover);
|
||||
|
||||
.filter {
|
||||
// Width is owned by ResizableBox (inline style).
|
||||
flex-shrink: 0;
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
@@ -85,14 +84,9 @@
|
||||
border: 1px solid var(--l1-border);
|
||||
background-color: var(--l1-background);
|
||||
|
||||
.resizable-box__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.quick-filters-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
width: 258px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,8 +101,6 @@
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
.trace-explorer.filters-expanded {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
@@ -40,8 +41,6 @@ import { isEmpty } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { TOOLBAR_VIEWS } from 'pages/TracesExplorer/constants';
|
||||
import { ResizableBox } from 'periscope/components/ResizableBox';
|
||||
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
|
||||
import { Warning } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -63,10 +62,6 @@ import TimeSeriesView from './TimeSeriesView';
|
||||
|
||||
import './TracesExplorer.styles.scss';
|
||||
|
||||
const QUICK_FILTERS_DEFAULT_WIDTH = 260;
|
||||
const QUICK_FILTERS_MIN_WIDTH = 240;
|
||||
const QUICK_FILTERS_MAX_WIDTH = 500;
|
||||
|
||||
function TracesExplorer(): JSX.Element {
|
||||
const {
|
||||
panelType,
|
||||
@@ -122,16 +117,6 @@ function TracesExplorer(): JSX.Element {
|
||||
const [warning, setWarning] = useState<Warning | undefined>();
|
||||
const [isOpen, setOpen] = useState<boolean>(true);
|
||||
|
||||
const {
|
||||
initialWidth: quickFiltersInitialWidth,
|
||||
persistWidth: persistQuickFiltersWidth,
|
||||
} = usePanelWidth({
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_TRACES,
|
||||
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
|
||||
minWidth: QUICK_FILTERS_MIN_WIDTH,
|
||||
maxWidth: QUICK_FILTERS_MAX_WIDTH,
|
||||
});
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
(): Query =>
|
||||
updateAllQueriesOperators(
|
||||
@@ -265,29 +250,16 @@ function TracesExplorer(): JSX.Element {
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="trace-explorer-page">
|
||||
{isOpen && (
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
|
||||
initialWidth={quickFiltersInitialWidth}
|
||||
minWidth={QUICK_FILTERS_MIN_WIDTH}
|
||||
maxWidth={QUICK_FILTERS_MAX_WIDTH}
|
||||
onResize={persistQuickFiltersWidth}
|
||||
resetToDefaultOnDoubleClick
|
||||
withHandle
|
||||
className="filter"
|
||||
handleTestId="quick-filters-resize-handle"
|
||||
>
|
||||
<QuickFilters
|
||||
className="qf-traces-explorer"
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
signal={SignalType.TRACES}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
/>
|
||||
</ResizableBox>
|
||||
)}
|
||||
<Card className="filter" hidden={!isOpen}>
|
||||
<QuickFilters
|
||||
className="qf-traces-explorer"
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
signal={SignalType.TRACES}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<div
|
||||
className={cx('trace-explorer', {
|
||||
'filters-expanded': isOpen,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.resizable-box {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&--disabled {
|
||||
flex: 1;
|
||||
@@ -17,17 +18,6 @@
|
||||
z-index: 10;
|
||||
background: var(--l2-border);
|
||||
|
||||
// Extend the interactive area beyond the 1px visual line so the handle
|
||||
// is easy to grab and double-click, without changing its appearance.
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: var(--primary);
|
||||
@@ -65,29 +55,4 @@
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Visible grip indicator (opt-in via the `withHandle` prop). Purely visual —
|
||||
// pointer events fall through to the handle so it still owns drag + reset.
|
||||
&__grip {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 26px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
background: var(--l1-background);
|
||||
color: var(--l2-foreground);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__handle:hover &__grip,
|
||||
&__handle:active &__grip {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { GripVertical } from '@signozhq/icons';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import './ResizableBox.styles.scss';
|
||||
@@ -12,28 +11,15 @@ export interface ResizableBoxProps {
|
||||
// resize (width). Dragging the handle away from the content grows the box;
|
||||
// dragging it toward the content shrinks it.
|
||||
handle?: ResizableBoxHandle;
|
||||
// Canonical default size, and the target that double-click reset restores to.
|
||||
defaultHeight?: number;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
defaultWidth?: number;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
// Starting size when different from the default (e.g. a persisted value).
|
||||
// Falls back to defaultWidth/defaultHeight when omitted, preserving the
|
||||
// behavior of callers that don't opt in.
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
// When true, double-clicking the handle resets the size to
|
||||
// defaultWidth/defaultHeight and fires onResize with that value.
|
||||
resetToDefaultOnDoubleClick?: boolean;
|
||||
// When true, renders a visible grip indicator on the handle so it is
|
||||
// discoverable as a draggable affordance.
|
||||
withHandle?: boolean;
|
||||
onResize?: (size: number) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
handleTestId?: string;
|
||||
}
|
||||
|
||||
function ResizableBox({
|
||||
@@ -45,22 +31,13 @@ function ResizableBox({
|
||||
defaultWidth = 200,
|
||||
minWidth = 50,
|
||||
maxWidth = Infinity,
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
resetToDefaultOnDoubleClick = false,
|
||||
withHandle = false,
|
||||
onResize,
|
||||
disabled = false,
|
||||
className,
|
||||
handleTestId,
|
||||
}: ResizableBoxProps): JSX.Element {
|
||||
const isHorizontal = handle === 'left' || handle === 'right';
|
||||
const isStartHandle = handle === 'top' || handle === 'left';
|
||||
const [size, setSize] = useState(
|
||||
isHorizontal
|
||||
? (initialWidth ?? defaultWidth)
|
||||
: (initialHeight ?? defaultHeight),
|
||||
);
|
||||
const [size, setSize] = useState(isHorizontal ? defaultWidth : defaultHeight);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
@@ -106,21 +83,6 @@ function ResizableBox({
|
||||
],
|
||||
);
|
||||
|
||||
const handleDoubleClick = useCallback((): void => {
|
||||
if (!resetToDefaultOnDoubleClick) {
|
||||
return;
|
||||
}
|
||||
const nextSize = isHorizontal ? defaultWidth : defaultHeight;
|
||||
setSize(nextSize);
|
||||
onResize?.(nextSize);
|
||||
}, [
|
||||
resetToDefaultOnDoubleClick,
|
||||
isHorizontal,
|
||||
defaultWidth,
|
||||
defaultHeight,
|
||||
onResize,
|
||||
]);
|
||||
|
||||
const containerStyle = disabled
|
||||
? undefined
|
||||
: isHorizontal
|
||||
@@ -137,22 +99,7 @@ function ResizableBox({
|
||||
style={containerStyle}
|
||||
>
|
||||
<div className="resizable-box__content">{children}</div>
|
||||
{!disabled && (
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}
|
||||
className={handleClass}
|
||||
onMouseDown={handleMouseDown}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
data-testid={handleTestId}
|
||||
>
|
||||
{withHandle && (
|
||||
<span className="resizable-box__grip">
|
||||
<GripVertical size={12} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!disabled && <div className={handleClass} onMouseDown={handleMouseDown} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import ResizableBox from '../ResizableBox';
|
||||
|
||||
const HANDLE_TEST_ID = 'resize-handle';
|
||||
|
||||
describe('ResizableBox', () => {
|
||||
it('starts at defaultWidth when initialWidth is omitted', () => {
|
||||
render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
|
||||
const box = screen.getByTestId(HANDLE_TEST_ID).parentElement as HTMLElement;
|
||||
expect(box.style.width).toBe('260px');
|
||||
});
|
||||
|
||||
it('starts at initialWidth when provided', () => {
|
||||
render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
initialWidth={340}
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
|
||||
const box = screen.getByTestId(HANDLE_TEST_ID).parentElement as HTMLElement;
|
||||
expect(box.style.width).toBe('340px');
|
||||
});
|
||||
|
||||
it('resets to defaultWidth and fires onResize on double-click when enabled', () => {
|
||||
const onResize = jest.fn();
|
||||
render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
initialWidth={480}
|
||||
onResize={onResize}
|
||||
resetToDefaultOnDoubleClick
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
|
||||
const handle = screen.getByTestId(HANDLE_TEST_ID);
|
||||
const box = handle.parentElement as HTMLElement;
|
||||
expect(box.style.width).toBe('480px');
|
||||
|
||||
fireEvent.doubleClick(handle);
|
||||
|
||||
expect(box.style.width).toBe('260px');
|
||||
expect(onResize).toHaveBeenCalledWith(260);
|
||||
});
|
||||
|
||||
it('does nothing on double-click when reset is not enabled', () => {
|
||||
const onResize = jest.fn();
|
||||
render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
initialWidth={480}
|
||||
onResize={onResize}
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
|
||||
const handle = screen.getByTestId(HANDLE_TEST_ID);
|
||||
const box = handle.parentElement as HTMLElement;
|
||||
|
||||
fireEvent.doubleClick(handle);
|
||||
|
||||
expect(box.style.width).toBe('480px');
|
||||
expect(onResize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders a visible grip only when withHandle is set', () => {
|
||||
const { rerender, container } = render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
expect(container.querySelector('.resizable-box__grip')).toBeNull();
|
||||
|
||||
rerender(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
withHandle
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
expect(container.querySelector('.resizable-box__grip')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('clamps drag to maxWidth and reports the clamped size via onResize', () => {
|
||||
const onResize = jest.fn();
|
||||
render(
|
||||
<ResizableBox
|
||||
handle="right"
|
||||
defaultWidth={260}
|
||||
minWidth={240}
|
||||
maxWidth={500}
|
||||
onResize={onResize}
|
||||
handleTestId={HANDLE_TEST_ID}
|
||||
>
|
||||
<div>content</div>
|
||||
</ResizableBox>,
|
||||
);
|
||||
|
||||
const handle = screen.getByTestId(HANDLE_TEST_ID);
|
||||
const box = handle.parentElement as HTMLElement;
|
||||
|
||||
fireEvent.mouseDown(handle, { clientX: 0 });
|
||||
fireEvent.mouseMove(document, { clientX: 1000 });
|
||||
fireEvent.mouseUp(document);
|
||||
|
||||
expect(box.style.width).toBe('500px');
|
||||
expect(onResize).toHaveBeenLastCalledWith(500);
|
||||
});
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
import usePanelWidth from '../usePanelWidth';
|
||||
|
||||
jest.mock('api/browser/localstorage/get');
|
||||
jest.mock('api/browser/localstorage/set');
|
||||
|
||||
const mockedGet = getLocalStorageKey as jest.MockedFunction<
|
||||
typeof getLocalStorageKey
|
||||
>;
|
||||
const mockedSet = setLocalStorageKey as jest.MockedFunction<
|
||||
typeof setLocalStorageKey
|
||||
>;
|
||||
|
||||
const ARGS = {
|
||||
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_LOGS,
|
||||
defaultWidth: 260,
|
||||
minWidth: 240,
|
||||
maxWidth: 500,
|
||||
};
|
||||
|
||||
describe('usePanelWidth', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns defaultWidth when nothing is persisted', () => {
|
||||
mockedGet.mockReturnValue(null);
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
expect(result.current.initialWidth).toBe(260);
|
||||
});
|
||||
|
||||
it('returns the persisted width when present', () => {
|
||||
mockedGet.mockReturnValue('340');
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
expect(result.current.initialWidth).toBe(340);
|
||||
});
|
||||
|
||||
it('clamps an out-of-bounds persisted width on read', () => {
|
||||
mockedGet.mockReturnValue('9999');
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
expect(result.current.initialWidth).toBe(500);
|
||||
});
|
||||
|
||||
it('falls back to defaultWidth for an invalid persisted value', () => {
|
||||
mockedGet.mockReturnValue('not-a-number');
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
expect(result.current.initialWidth).toBe(260);
|
||||
});
|
||||
|
||||
it('persists a clamped width (debounced)', () => {
|
||||
mockedGet.mockReturnValue(null);
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
|
||||
act(() => {
|
||||
result.current.persistWidth(320);
|
||||
jest.advanceTimersByTime(200);
|
||||
});
|
||||
|
||||
expect(mockedSet).toHaveBeenCalledWith(
|
||||
LOCALSTORAGE.QUICK_FILTERS_WIDTH_LOGS,
|
||||
'320',
|
||||
);
|
||||
});
|
||||
|
||||
it('clamps below-min widths before persisting', () => {
|
||||
mockedGet.mockReturnValue(null);
|
||||
const { result } = renderHook(() => usePanelWidth(ARGS));
|
||||
|
||||
act(() => {
|
||||
result.current.persistWidth(10);
|
||||
jest.advanceTimersByTime(200);
|
||||
});
|
||||
|
||||
expect(mockedSet).toHaveBeenCalledWith(
|
||||
LOCALSTORAGE.QUICK_FILTERS_WIDTH_LOGS,
|
||||
'240',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
const PERSIST_DEBOUNCE_MS = 150;
|
||||
|
||||
interface UsePanelWidthArgs {
|
||||
/** Per-page localStorage key the width is persisted under. */
|
||||
storageKey: LOCALSTORAGE;
|
||||
/** Canonical default width, used when nothing is persisted. */
|
||||
defaultWidth: number;
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
}
|
||||
|
||||
interface UsePanelWidthReturn {
|
||||
/** Width to start from: the persisted value (clamped) or the default. */
|
||||
initialWidth: number;
|
||||
/** Clamp and persist a width. Debounced to avoid a write per mousemove. */
|
||||
persistWidth: (width: number) => void;
|
||||
}
|
||||
|
||||
const clamp = (value: number, min: number, max: number): number =>
|
||||
Math.min(max, Math.max(min, value));
|
||||
|
||||
/**
|
||||
* Per-page localStorage persistence for a resizable panel width. Mirrors the
|
||||
* getLocalStorageKey/setLocalStorageKey idiom used for the trace span-details
|
||||
* panel position. Pairs with ResizableBox: feed initialWidth into its
|
||||
* initialWidth prop and persistWidth into its onResize.
|
||||
*/
|
||||
function usePanelWidth({
|
||||
storageKey,
|
||||
defaultWidth,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
}: UsePanelWidthArgs): UsePanelWidthReturn {
|
||||
// Read once on mount. Kept in a ref so a re-render doesn't re-read storage.
|
||||
const initialWidthRef = useRef<number | null>(null);
|
||||
if (initialWidthRef.current === null) {
|
||||
const stored = getLocalStorageKey(storageKey);
|
||||
const parsed = stored !== null && stored !== '' ? Number(stored) : NaN;
|
||||
initialWidthRef.current = Number.isFinite(parsed)
|
||||
? clamp(parsed, minWidth, maxWidth)
|
||||
: defaultWidth;
|
||||
}
|
||||
|
||||
const debouncedWrite = useMemo(
|
||||
() =>
|
||||
debounce((width: number): void => {
|
||||
setLocalStorageKey(storageKey, String(width));
|
||||
}, PERSIST_DEBOUNCE_MS),
|
||||
[storageKey],
|
||||
);
|
||||
|
||||
const persistWidth = useCallback(
|
||||
(width: number): void => {
|
||||
debouncedWrite(clamp(width, minWidth, maxWidth));
|
||||
},
|
||||
[debouncedWrite, minWidth, maxWidth],
|
||||
);
|
||||
|
||||
return { initialWidth: initialWidthRef.current, persistWidth };
|
||||
}
|
||||
|
||||
export default usePanelWidth;
|
||||
@@ -71,7 +71,7 @@ func (module *module) ListV2(ctx context.Context, orgID valuer.UUID, params *das
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewListableDashboardV2(dashboards, total, tagsByDashboard, allTags), nil
|
||||
return dashboardtypes.NewListableDashboardV2(dashboards, total, tagsByDashboard, allTags)
|
||||
}
|
||||
|
||||
func (module *module) ListForUserV2(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, params *dashboardtypes.ListDashboardsV2Params) (*dashboardtypes.ListableDashboardForUserV2, error) {
|
||||
@@ -90,7 +90,7 @@ func (module *module) ListForUserV2(ctx context.Context, orgID valuer.UUID, user
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewListableDashboardForUserV2(rows, total, tagsByDashboard, allTags), nil
|
||||
return dashboardtypes.NewListableDashboardForUserV2(rows, total, tagsByDashboard, allTags)
|
||||
}
|
||||
|
||||
func (module *module) fetchDashboardTags(ctx context.Context, orgID valuer.UUID, dashboardIDs []valuer.UUID) (map[valuer.UUID][]*tagtypes.Tag, []*tagtypes.Tag, error) {
|
||||
|
||||
@@ -60,7 +60,7 @@ func newConfig() factory.Config {
|
||||
Path: "/var/lib/signoz/signoz.db",
|
||||
Mode: "wal",
|
||||
BusyTimeout: 10000 * time.Millisecond, // increasing the defaults from https://github.com/mattn/go-sqlite3/blob/master/sqlite3.go#L1098 because of transpilation from C to GO
|
||||
TransactionMode: "deferred",
|
||||
TransactionMode: "immediate",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -64,13 +64,6 @@ type (
|
||||
ListableDashboard []*GettableDashboard
|
||||
)
|
||||
|
||||
// readString reads a string field from the untyped data blob, yielding "" when
|
||||
// the key is absent, null, or not a string.
|
||||
func (d StorableDashboardData) readString(key string) string {
|
||||
s, _ := d[key].(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func NewStorableDashboardFromDashboard(dashboard *Dashboard) (*StorableDashboard, error) {
|
||||
dashboardID, err := valuer.NewUUID(dashboard.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -122,10 +122,6 @@ type listedDashboardV2 struct {
|
||||
Image string `json:"image,omitempty"`
|
||||
Tags []*tagtypes.GettableTag `json:"tags" required:"true" nullable:"false"`
|
||||
Spec listedDashboardV2Spec `json:"spec" required:"true"`
|
||||
// Legacy marks a dashboard whose stored data is not yet in the v2 (perses)
|
||||
// schema. Such rows are extracted best-effort from the v1 shape so the list
|
||||
// still surfaces them; callers should route them to the legacy view.
|
||||
Legacy bool `json:"legacy" required:"true"`
|
||||
}
|
||||
|
||||
type listedDashboardV2Spec struct {
|
||||
@@ -148,41 +144,6 @@ func newListedDashboardV2(v2 *DashboardV2) *listedDashboardV2 {
|
||||
}
|
||||
}
|
||||
|
||||
// newListedDashboardForList builds the list view for a single dashboard. A row
|
||||
// whose stored data is not in the v2 (perses) schema is extracted best-effort
|
||||
// from the v1 shape and flagged Legacy, so one legacy or malformed dashboard
|
||||
// can't fail the whole list.
|
||||
func newListedDashboardForList(storable *StorableDashboard, tags []*tagtypes.Tag) *listedDashboardV2 {
|
||||
if v2, err := storable.ToDashboardV2(tags); err == nil {
|
||||
return newListedDashboardV2(v2)
|
||||
}
|
||||
return newLegacyListedDashboardV2(storable, tags)
|
||||
}
|
||||
|
||||
// newLegacyListedDashboardV2 pulls the display-relevant fields out of a v1
|
||||
// dashboard blob. Column-backed fields (name, source, timestamps, …) come
|
||||
// straight off the row; title/description/image/version are read leniently from
|
||||
// the untyped v1 data, defaulting to zero when absent or of the wrong type.
|
||||
func newLegacyListedDashboardV2(storable *StorableDashboard, tags []*tagtypes.Tag) *listedDashboardV2 {
|
||||
return &listedDashboardV2{
|
||||
Identifiable: storable.Identifiable,
|
||||
TimeAuditable: storable.TimeAuditable,
|
||||
UserAuditable: storable.UserAuditable,
|
||||
OrgID: storable.OrgID,
|
||||
Locked: storable.Locked,
|
||||
Source: storable.Source,
|
||||
SchemaVersion: storable.Data.readString("version"),
|
||||
Name: storable.Name,
|
||||
Image: storable.Data.readString("image"),
|
||||
Tags: tagtypes.NewGettableTagsFromTags(tags),
|
||||
Spec: listedDashboardV2Spec{Display: Display{
|
||||
Name: storable.Data.readString("title"),
|
||||
Description: storable.Data.readString("description"),
|
||||
}},
|
||||
Legacy: true,
|
||||
}
|
||||
}
|
||||
|
||||
type ListableDashboardV2 struct {
|
||||
Dashboards []*listedDashboardV2 `json:"dashboards" required:"true" nullable:"false"`
|
||||
Total int64 `json:"total" required:"true"`
|
||||
@@ -190,17 +151,21 @@ type ListableDashboardV2 struct {
|
||||
ReservedKeywords []DSLKey `json:"reservedKeywords" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
func NewListableDashboardV2(dashboards []*StorableDashboard, total int64, tagsByEntity map[valuer.UUID][]*tagtypes.Tag, allTags []*tagtypes.Tag) *ListableDashboardV2 {
|
||||
func NewListableDashboardV2(dashboards []*StorableDashboard, total int64, tagsByEntity map[valuer.UUID][]*tagtypes.Tag, allTags []*tagtypes.Tag) (*ListableDashboardV2, error) {
|
||||
items := make([]*listedDashboardV2, len(dashboards))
|
||||
for i, d := range dashboards {
|
||||
items[i] = newListedDashboardForList(d, tagsByEntity[d.ID])
|
||||
v2, err := d.ToDashboardV2(tagsByEntity[d.ID])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items[i] = newListedDashboardV2(v2)
|
||||
}
|
||||
return &ListableDashboardV2{
|
||||
Dashboards: items,
|
||||
Total: total,
|
||||
Tags: tagtypes.NewGettableTagsFromTags(allTags),
|
||||
ReservedKeywords: ReservedFilterKeys(),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// listedDashboardForUserV2 is a listed dashboard plus the calling user's pin
|
||||
@@ -225,11 +190,15 @@ type StorableDashboardWithPinInfo struct {
|
||||
Pinned bool
|
||||
}
|
||||
|
||||
func NewListableDashboardForUserV2(rows []*StorableDashboardWithPinInfo, total int64, tagsByEntity map[valuer.UUID][]*tagtypes.Tag, allTags []*tagtypes.Tag) *ListableDashboardForUserV2 {
|
||||
func NewListableDashboardForUserV2(rows []*StorableDashboardWithPinInfo, total int64, tagsByEntity map[valuer.UUID][]*tagtypes.Tag, allTags []*tagtypes.Tag) (*ListableDashboardForUserV2, error) {
|
||||
items := make([]*listedDashboardForUserV2, len(rows))
|
||||
for i, r := range rows {
|
||||
v2, err := r.Dashboard.ToDashboardV2(tagsByEntity[r.Dashboard.ID])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items[i] = &listedDashboardForUserV2{
|
||||
listedDashboardV2: *newListedDashboardForList(r.Dashboard, tagsByEntity[r.Dashboard.ID]),
|
||||
listedDashboardV2: *newListedDashboardV2(v2),
|
||||
Pinned: r.Pinned,
|
||||
}
|
||||
}
|
||||
@@ -238,5 +207,5 @@ func NewListableDashboardForUserV2(rows []*StorableDashboardWithPinInfo, total i
|
||||
Total: total,
|
||||
Tags: tagtypes.NewGettableTagsFromTags(allTags),
|
||||
ReservedKeywords: ReservedFilterKeys(),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -305,146 +305,6 @@ func TestNextCloneDisplayName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLegacyListedDashboardV2(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
dashboardID := valuer.GenerateUUID()
|
||||
createdAt := time.Date(2026, time.January, 1, 12, 0, 0, 0, time.UTC)
|
||||
updatedAt := time.Date(2026, time.January, 2, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
t.Run("extracts display fields from a well-formed v1 blob", func(t *testing.T) {
|
||||
storable := &StorableDashboard{
|
||||
Identifiable: types.Identifiable{ID: dashboardID},
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: createdAt, UpdatedAt: updatedAt},
|
||||
UserAuditable: types.UserAuditable{CreatedBy: "alice", UpdatedBy: "bob"},
|
||||
OrgID: orgID,
|
||||
Locked: true,
|
||||
Source: SourceUser,
|
||||
Name: "legacy-dashboard",
|
||||
Data: StorableDashboardData{
|
||||
"title": "Legacy Title",
|
||||
"description": "an old v1 dashboard",
|
||||
"image": "data:image/png;base64,xyz",
|
||||
"version": "v5",
|
||||
"widgets": []any{},
|
||||
},
|
||||
}
|
||||
listed := newLegacyListedDashboardV2(storable, nil)
|
||||
|
||||
assert.True(t, listed.Legacy, "a non-v2 dashboard must be flagged legacy")
|
||||
assert.Equal(t, storable.Identifiable, listed.Identifiable)
|
||||
assert.Equal(t, storable.TimeAuditable, listed.TimeAuditable)
|
||||
assert.Equal(t, storable.UserAuditable, listed.UserAuditable)
|
||||
assert.Equal(t, orgID, listed.OrgID)
|
||||
assert.True(t, listed.Locked)
|
||||
assert.Equal(t, SourceUser, listed.Source)
|
||||
assert.Equal(t, "legacy-dashboard", listed.Name, "name comes off the column, not the blob")
|
||||
assert.Equal(t, "v5", listed.SchemaVersion)
|
||||
assert.Equal(t, "data:image/png;base64,xyz", listed.Image)
|
||||
assert.Equal(t, "Legacy Title", listed.Spec.Display.Name)
|
||||
assert.Equal(t, "an old v1 dashboard", listed.Spec.Display.Description)
|
||||
assert.Empty(t, listed.Tags, "v1 dashboards predate tags; nil converts to an empty, non-nil slice")
|
||||
})
|
||||
|
||||
t.Run("yields zero values for absent or wrongly-typed fields", func(t *testing.T) {
|
||||
storable := &StorableDashboard{
|
||||
OrgID: orgID,
|
||||
Source: SourceUser,
|
||||
Data: StorableDashboardData{
|
||||
"title": 42, // wrong type
|
||||
"version": []any{"v5"}, // wrong type
|
||||
"image": nil, // null
|
||||
},
|
||||
}
|
||||
|
||||
listed := newLegacyListedDashboardV2(storable, nil)
|
||||
|
||||
assert.True(t, listed.Legacy)
|
||||
assert.Empty(t, listed.Spec.Display.Name)
|
||||
assert.Empty(t, listed.SchemaVersion)
|
||||
assert.Empty(t, listed.Image)
|
||||
assert.Empty(t, listed.Tags, "nil tags convert to an empty, non-nil slice")
|
||||
})
|
||||
|
||||
t.Run("tolerates entirely empty data", func(t *testing.T) {
|
||||
storable := &StorableDashboard{OrgID: orgID, Source: SourceUser, Name: "bare"}
|
||||
|
||||
listed := newLegacyListedDashboardV2(storable, nil)
|
||||
|
||||
assert.True(t, listed.Legacy)
|
||||
assert.Equal(t, "bare", listed.Name)
|
||||
assert.Empty(t, listed.Spec.Display.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewListableDashboardV2MixedSchemas(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
v2Dashboard := newTestDashboardV2(t, orgID, SourceUser)
|
||||
v2Storable, err := v2Dashboard.ToStorableDashboard()
|
||||
require.NoError(t, err)
|
||||
|
||||
v1Storable := &StorableDashboard{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
OrgID: orgID,
|
||||
Source: SourceUser,
|
||||
Name: "legacy-dashboard",
|
||||
Data: StorableDashboardData{
|
||||
"title": "Legacy Title",
|
||||
"version": "v5",
|
||||
"widgets": []any{},
|
||||
},
|
||||
}
|
||||
|
||||
tagsByEntity := map[valuer.UUID][]*tagtypes.Tag{
|
||||
v2Storable.ID: v2Dashboard.Tags,
|
||||
}
|
||||
|
||||
listable := NewListableDashboardV2([]*StorableDashboard{v2Storable, v1Storable}, 2, tagsByEntity, nil)
|
||||
|
||||
require.Len(t, listable.Dashboards, 2, "a single legacy dashboard must not drop rows from the list")
|
||||
assert.Equal(t, int64(2), listable.Total)
|
||||
|
||||
v2Row := listable.Dashboards[0]
|
||||
assert.False(t, v2Row.Legacy, "a v2 dashboard is not legacy")
|
||||
assert.Equal(t, SchemaVersion, v2Row.SchemaVersion)
|
||||
assert.Equal(t, "Test Dashboard", v2Row.Spec.Display.Name)
|
||||
|
||||
v1Row := listable.Dashboards[1]
|
||||
assert.True(t, v1Row.Legacy, "a v1 dashboard is flagged legacy")
|
||||
assert.Equal(t, "v5", v1Row.SchemaVersion)
|
||||
assert.Equal(t, "Legacy Title", v1Row.Spec.Display.Name)
|
||||
assert.Equal(t, "legacy-dashboard", v1Row.Name)
|
||||
}
|
||||
|
||||
func TestNewListableDashboardForUserV2MixedSchemas(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
v2Dashboard := newTestDashboardV2(t, orgID, SourceUser)
|
||||
v2Storable, err := v2Dashboard.ToStorableDashboard()
|
||||
require.NoError(t, err)
|
||||
|
||||
v1Storable := &StorableDashboard{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
OrgID: orgID,
|
||||
Source: SourceUser,
|
||||
Name: "legacy-dashboard",
|
||||
Data: StorableDashboardData{"title": "Legacy Title", "version": "v5"},
|
||||
}
|
||||
|
||||
rows := []*StorableDashboardWithPinInfo{
|
||||
{Dashboard: v2Storable, Pinned: true},
|
||||
{Dashboard: v1Storable, Pinned: false},
|
||||
}
|
||||
|
||||
listable := NewListableDashboardForUserV2(rows, 2, nil, nil)
|
||||
|
||||
require.Len(t, listable.Dashboards, 2)
|
||||
assert.False(t, listable.Dashboards[0].Legacy)
|
||||
assert.True(t, listable.Dashboards[0].Pinned)
|
||||
assert.True(t, listable.Dashboards[1].Legacy)
|
||||
assert.False(t, listable.Dashboards[1].Pinned)
|
||||
}
|
||||
|
||||
func TestDashboardV2StorableRoundTrip(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
original := newTestDashboardV2(t, orgID, SourceIntegration)
|
||||
|
||||
@@ -62,12 +62,6 @@ def pytest_addoption(parser: pytest.Parser):
|
||||
default="delete",
|
||||
help="sqlite mode",
|
||||
)
|
||||
parser.addoption(
|
||||
"--sqlite-transaction-mode",
|
||||
action="store",
|
||||
default="deferred",
|
||||
help="sqlite transaction mode",
|
||||
)
|
||||
parser.addoption(
|
||||
"--postgres-version",
|
||||
action="store",
|
||||
|
||||
2
tests/fixtures/sqlite.py
vendored
2
tests/fixtures/sqlite.py
vendored
@@ -30,7 +30,6 @@ def sqlite(
|
||||
assert result.fetchone()[0] == 1
|
||||
|
||||
mode = pytestconfig.getoption("--sqlite-mode")
|
||||
transaction_mode = pytestconfig.getoption("--sqlite-transaction-mode")
|
||||
return types.TestContainerSQL(
|
||||
container=types.TestContainerDocker(
|
||||
id="",
|
||||
@@ -42,7 +41,6 @@ def sqlite(
|
||||
"SIGNOZ_SQLSTORE_PROVIDER": "sqlite",
|
||||
"SIGNOZ_SQLSTORE_SQLITE_PATH": str(path),
|
||||
"SIGNOZ_SQLSTORE_SQLITE_MODE": mode,
|
||||
"SIGNOZ_SQLSTORE_SQLITE_TRANSACTION__MODE": transaction_mode,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user