mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-22 00:00:27 +01:00
Compare commits
17 Commits
v0.76.0-cl
...
tpapi-demo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a13d1c8954 | ||
|
|
a564e5fd21 | ||
|
|
d85b8217e7 | ||
|
|
c9d2e381a8 | ||
|
|
57778cafba | ||
|
|
a056fa7a6f | ||
|
|
1747868d76 | ||
|
|
5497464548 | ||
|
|
946a249c85 | ||
|
|
06be0f4330 | ||
|
|
5dba1f3dbb | ||
|
|
a9618886b9 | ||
|
|
aac2647b67 | ||
|
|
785a0f2a48 | ||
|
|
b46adb5927 | ||
|
|
ae87499679 | ||
|
|
f0b0889d2e |
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "e2e",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.22.0",
|
||||
"@types/node": "^20.9.2"
|
||||
},
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"dotenv": "8.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
|
||||
fullyParallel: true,
|
||||
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
name: "Signoz E2E",
|
||||
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
|
||||
reporter: process.env.CI ? "github" : "list",
|
||||
|
||||
preserveOutput: "always",
|
||||
|
||||
updateSnapshots: "all",
|
||||
|
||||
quiet: false,
|
||||
|
||||
testMatch: ["**/*.spec.ts"],
|
||||
|
||||
use: {
|
||||
trace: "on-first-retry",
|
||||
|
||||
baseURL:
|
||||
process.env.PLAYWRIGHT_TEST_BASE_URL || "https://stagingapp.signoz.io/",
|
||||
},
|
||||
|
||||
projects: [
|
||||
{ name: "setup", testMatch: /.*\.setup\.ts/ },
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
// Use prepared auth state.
|
||||
storageState: ".auth/user.json",
|
||||
},
|
||||
dependencies: ["setup"],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import ROUTES from "../../frontend/src/constants/routes";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const authFile = ".auth/user.json";
|
||||
|
||||
test("E2E Login Test", async ({ page }) => {
|
||||
await Promise.all([page.goto("/"), page.waitForRequest("**/version")]);
|
||||
|
||||
const signup = "Monitor your applications. Find what is causing issues.";
|
||||
|
||||
const el = await page.locator(`text=${signup}`);
|
||||
|
||||
expect(el).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator("id=loginEmail")
|
||||
.type(
|
||||
process.env.PLAYWRIGHT_USERNAME ? process.env.PLAYWRIGHT_USERNAME : ""
|
||||
);
|
||||
|
||||
await page.getByText("Next").click();
|
||||
|
||||
await page
|
||||
.locator('input[id="currentPassword"]')
|
||||
.fill(
|
||||
process.env.PLAYWRIGHT_PASSWORD ? process.env.PLAYWRIGHT_PASSWORD : ""
|
||||
);
|
||||
|
||||
await page.locator('button[data-attr="signup"]').click();
|
||||
|
||||
await expect(page).toHaveURL(ROUTES.APPLICATION);
|
||||
|
||||
await page.context().storageState({ path: authFile });
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
export const SERVICE_TABLE_HEADERS = {
|
||||
APPLICATION: "Applicaton",
|
||||
P99LATENCY: "P99 latency (in ms)",
|
||||
ERROR_RATE: "Error Rate (% of total)",
|
||||
OPS_PER_SECOND: "Operations Per Second",
|
||||
};
|
||||
|
||||
export const DATA_TEST_IDS = {
|
||||
NEW_DASHBOARD_BTN: "create-new-dashboard",
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import ROUTES from "../../frontend/src/constants/routes";
|
||||
import { DATA_TEST_IDS, SERVICE_TABLE_HEADERS } from "./contants";
|
||||
|
||||
test("Basic Navigation Check across different resources", async ({ page }) => {
|
||||
// route to services page and check if the page renders fine with BE contract
|
||||
await Promise.all([
|
||||
page.goto(ROUTES.APPLICATION),
|
||||
page.waitForRequest("**/v1/services"),
|
||||
]);
|
||||
|
||||
const p99Latency = page.locator(
|
||||
`th:has-text("${SERVICE_TABLE_HEADERS.P99LATENCY}")`
|
||||
);
|
||||
|
||||
await expect(p99Latency).toBeVisible();
|
||||
|
||||
// route to the new trace explorer page and check if the page renders fine
|
||||
await page.goto(ROUTES.TRACES_EXPLORER);
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const listViewTable = await page
|
||||
.locator('div[role="presentation"]')
|
||||
.isVisible();
|
||||
|
||||
expect(listViewTable).toBeTruthy();
|
||||
|
||||
// route to the dashboards page and check if the page renders fine
|
||||
await Promise.all([
|
||||
page.goto(ROUTES.ALL_DASHBOARD),
|
||||
page.waitForRequest("**/v1/dashboards"),
|
||||
]);
|
||||
|
||||
const newDashboardBtn = await page
|
||||
.locator(`data-testid=${DATA_TEST_IDS.NEW_DASHBOARD_BTN}`)
|
||||
.isVisible();
|
||||
|
||||
expect(newDashboardBtn).toBeTruthy();
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@playwright/test@^1.22.0":
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.40.0.tgz#d06c506977dd7863aa16e07f2136351ecc1be6ed"
|
||||
integrity sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==
|
||||
dependencies:
|
||||
playwright "1.40.0"
|
||||
|
||||
"@types/node@^20.9.2":
|
||||
version "20.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.2.tgz#002815c8e87fe0c9369121c78b52e800fadc0ac6"
|
||||
integrity sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
dotenv@8.2.0:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
||||
|
||||
fsevents@2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
playwright-core@1.40.0:
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.40.0.tgz#82f61e5504cb3097803b6f8bbd98190dd34bdf14"
|
||||
integrity sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==
|
||||
|
||||
playwright@1.40.0:
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.40.0.tgz#2a1824b9fe5c4fe52ed53db9ea68003543a99df0"
|
||||
integrity sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==
|
||||
dependencies:
|
||||
playwright-core "1.40.0"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
@@ -1,15 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/pkg/errors"
|
||||
"go.signoz.io/signoz/pkg/http/render"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -31,26 +31,31 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// Get the dashboard UUID from the request
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
if strings.HasPrefix(uuid,"integration") {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: errors.New("dashboards created by integrations cannot be unlocked")}, "You are not authorized to lock/unlock this dashboard")
|
||||
return
|
||||
}
|
||||
dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
||||
if strings.HasPrefix(uuid, "integration") {
|
||||
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "dashboards created by integrations cannot be modified"))
|
||||
return
|
||||
}
|
||||
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
if !auth.IsAdmin(user) && (dashboard.CreateBy != nil && *dashboard.CreateBy != user.Email) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: err}, "You are not authorized to lock/unlock this dashboard")
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
dashboard, err := dashboards.GetDashboard(r.Context(), claims.OrgID, uuid)
|
||||
if err != nil {
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get dashboard"))
|
||||
return
|
||||
}
|
||||
|
||||
if !auth.IsAdminV2(claims) && (dashboard.CreatedBy != claims.Email) {
|
||||
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "You are not authorized to lock/unlock this dashboard"))
|
||||
return
|
||||
}
|
||||
|
||||
// Lock/Unlock the dashboard
|
||||
err = dashboards.LockUnlockDashboard(r.Context(), uuid, lock)
|
||||
err = dashboards.LockUnlockDashboard(r.Context(), claims.OrgID, uuid, lock)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to lock/unlock dashboard"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := baseexplorer.InitWithDSN(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil {
|
||||
if err := baseexplorer.InitWithDSN(serverOptions.SigNoz.SQLStore.BunDB()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil {
|
||||
if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore.BunDB()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^10.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "0.379.0",
|
||||
"lucide-react": "0.427.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
|
||||
@@ -273,3 +273,7 @@ export const MetricsExplorer = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "MetricsExplorer" */ 'pages/MetricsExplorer'),
|
||||
);
|
||||
|
||||
export const ApiMonitoring = Loadable(
|
||||
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
APIKeys,
|
||||
ApiMonitoring,
|
||||
BillingPage,
|
||||
CreateAlertChannelAlerts,
|
||||
CreateNewAlerts,
|
||||
@@ -465,6 +466,13 @@ const routes: AppRoutes[] = [
|
||||
key: 'METRICS_EXPLORER_VIEWS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.API_MONITORING,
|
||||
exact: true,
|
||||
component: ApiMonitoring,
|
||||
key: 'API_MONITORING',
|
||||
isPrivate: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const SUPPORT_ROUTE: AppRoutes = {
|
||||
|
||||
@@ -63,30 +63,31 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="quick-filters">
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING && (
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
<Typography.Text className="text">Filters for</Typography.Text>
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
</section>
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING &&
|
||||
source !== QuickFiltersSource.API_MONITORING && (
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
<Typography.Text className="text">Filters for</Typography.Text>
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
</section>
|
||||
|
||||
<section className="right-actions">
|
||||
<Tooltip title="Reset All">
|
||||
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||
</Tooltip>
|
||||
<div className="divider-filter" />
|
||||
<Tooltip title="Collapse Filters">
|
||||
<VerticalAlignTopOutlined
|
||||
rotate={270}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
<section className="right-actions">
|
||||
<Tooltip title="Reset All">
|
||||
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||
</Tooltip>
|
||||
<div className="divider-filter" />
|
||||
<Tooltip title="Collapse Filters">
|
||||
<VerticalAlignTopOutlined
|
||||
rotate={270}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
)}
|
||||
|
||||
<section className="filters">
|
||||
{config.map((filter) => {
|
||||
|
||||
@@ -39,4 +39,5 @@ export enum QuickFiltersSource {
|
||||
LOGS_EXPLORER = 'logs-explorer',
|
||||
INFRA_MONITORING = 'infra-monitoring',
|
||||
TRACES_EXPLORER = 'traces-explorer',
|
||||
API_MONITORING = 'api-monitoring',
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ const ROUTES = {
|
||||
METRICS_EXPLORER: '/metrics-explorer/summary',
|
||||
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
||||
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
||||
API_MONITORING: '/api-monitoring/explorer',
|
||||
} as const;
|
||||
|
||||
export default ROUTES;
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Select, Spin, Table, Typography } from 'antd';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import {
|
||||
EndPointsTableRowData,
|
||||
formatEndPointsDataForTable,
|
||||
getEndPointsColumnsConfig,
|
||||
getEndPointsQueryPayload,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
import ExpandedRow from './ExpandedRow';
|
||||
|
||||
function AllEndPoints({
|
||||
domainName,
|
||||
setSelectedEndPointName,
|
||||
setSelectedView,
|
||||
}: {
|
||||
domainName: string;
|
||||
setSelectedEndPointName: (name: string) => void;
|
||||
setSelectedView: (tab: VIEWS) => void;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys({
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateAttribute: '',
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
});
|
||||
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
|
||||
const [groupByOptions, setGroupByOptions] = useState<
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const groupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(key) => key.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
groupBy.push(key);
|
||||
}
|
||||
}
|
||||
setGroupBy(groupBy);
|
||||
},
|
||||
[groupByFiltersData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() =>
|
||||
getEndPointsQueryPayload(
|
||||
groupBy,
|
||||
domainName,
|
||||
Math.floor(minTime / 1e9),
|
||||
Math.floor(maxTime / 1e9),
|
||||
),
|
||||
[groupBy, domainName, minTime, maxTime],
|
||||
);
|
||||
|
||||
const endPointsDataQueries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryKey: ['domain-all-endpoints', payload, ENTITY_VERSION_V4, groupBy],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
staleTime: 60 * 1000, // 1 minute stale time : optimize this part
|
||||
})),
|
||||
);
|
||||
|
||||
const endPointsDataQuery = endPointsDataQueries[0];
|
||||
|
||||
const endPointsColumnsConfig = useMemo(
|
||||
() => getEndPointsColumnsConfig(groupBy.length > 0, expandedRowKeys),
|
||||
[groupBy.length, expandedRowKeys],
|
||||
);
|
||||
|
||||
const expandedRowRender = (record: EndPointsTableRowData): JSX.Element => (
|
||||
<ExpandedRow
|
||||
domainName={domainName}
|
||||
selectedRowData={record}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
setSelectedView={setSelectedView}
|
||||
/>
|
||||
);
|
||||
|
||||
const handleGroupByRowClick = (record: EndPointsTableRowData): void => {
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys((expandedRowKeys) => [...expandedRowKeys, record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowClick = (record: EndPointsTableRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedEndPointName(record.endpointName); // this will open up the endpoint details tab
|
||||
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
|
||||
} else {
|
||||
handleGroupByRowClick(record); // this will prepare the nested query payload
|
||||
}
|
||||
};
|
||||
|
||||
const formattedEndPointsData = useMemo(
|
||||
() =>
|
||||
formatEndPointsDataForTable(
|
||||
endPointsDataQuery.data?.payload.data.result[0].table.rows,
|
||||
groupBy,
|
||||
),
|
||||
[groupBy, endPointsDataQuery.data],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="all-endpoints-container">
|
||||
<div className="group-by-container">
|
||||
<div className="group-by-label"> Group by </div>
|
||||
<Select
|
||||
className="group-by-select"
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="Search for attribute"
|
||||
style={{ width: '100%' }}
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
/>{' '}
|
||||
</div>
|
||||
<div className="endpoints-table-container">
|
||||
<div className="endpoints-table-header">Endpoint overview</div>
|
||||
<Table
|
||||
columns={endPointsColumnsConfig}
|
||||
loading={{
|
||||
spinning: endPointsDataQuery.isFetching || endPointsDataQuery.isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
dataSource={
|
||||
endPointsDataQuery.isFetching || endPointsDataQuery.isLoading
|
||||
? []
|
||||
: formattedEndPointsData
|
||||
}
|
||||
locale={{
|
||||
emptyText:
|
||||
endPointsDataQuery.isFetching || endPointsDataQuery.isLoading ? null : (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: groupBy.length > 0 ? expandedRowRender : undefined,
|
||||
expandedRowKeys,
|
||||
expandIconColumnIndex: -1,
|
||||
}}
|
||||
rowClassName={(_, index): string =>
|
||||
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AllEndPoints;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
import './DomainDetails.styles.scss';
|
||||
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Divider, Drawer, Progress, Radio, Tooltip, Typography } from 'antd';
|
||||
import { RadioChangeEvent } from 'antd/lib';
|
||||
import { getLastUsedRelativeTime } from 'container/ApiMonitoring/utils';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import AllEndPoints from './AllEndPoints';
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
import EndPointDetails from './EndPointDetails';
|
||||
|
||||
function DomainDetails({
|
||||
domainData,
|
||||
handleClose,
|
||||
}: {
|
||||
domainData: any;
|
||||
handleClose: () => void;
|
||||
}): JSX.Element {
|
||||
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.ALL_ENDPOINTS);
|
||||
const [selectedEndPointName, setSelectedEndPointName] = useState<string>('');
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="60%"
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">
|
||||
{domainData.domainName}
|
||||
</Typography.Text>
|
||||
{/* add the navigation buttons for domain */}
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!domainData}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{domainData && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
EXTERNAL API
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
AVERAGE LATENCY
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
ERROR RATE
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
LAST USED
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={domainData.endpointCount}>
|
||||
<span className="round-metric-tag">{domainData.endpointCount}</span>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
{/* // update the tooltip as well */}
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={domainData.latency}>
|
||||
<span className="round-metric-tag">
|
||||
{(domainData.latency / 1000).toFixed(3)}s
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
{/* // update the tooltip as well */}
|
||||
<Typography.Text className="entity-details-metadata-value error-rate">
|
||||
<Tooltip title={domainData.errorRate}>
|
||||
<Progress
|
||||
percent={Number((domainData.errorRate * 100).toFixed(1))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number(
|
||||
(domainData.errorRate * 100).toFixed(1),
|
||||
);
|
||||
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
|
||||
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
{/* // update the tooltip as well */}
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={domainData.lastUsed}>
|
||||
{getLastUsedRelativeTime(domainData.lastUsed)}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedView === VIEW_TYPES.ALL_ENDPOINTS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.ALL_ENDPOINTS}
|
||||
>
|
||||
<div className="view-title">All Endpoints</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.ENDPOINT_DETAILS
|
||||
? 'tab selected_view'
|
||||
: 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.ENDPOINT_DETAILS}
|
||||
>
|
||||
<div className="view-title">Endpoint Details</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
|
||||
<AllEndPoints
|
||||
domainName={domainData.domainName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
setSelectedView={setSelectedView}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView === VIEW_TYPES.ENDPOINT_DETAILS && (
|
||||
<EndPointDetails
|
||||
domainName={domainData.domainName}
|
||||
endPointName={selectedEndPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default DomainDetails;
|
||||
@@ -0,0 +1,126 @@
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { getEndPointDetailsQueryPayload } from 'container/ApiMonitoring/utils';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import DependentServices from './components/DependentServices';
|
||||
import EndPointDetailsZeroState from './components/EndPointDetailsZeroState';
|
||||
import EndPointMetrics from './components/EndPointMetrics';
|
||||
import EndPointsDropDown from './components/EndPointsDropDown';
|
||||
import MetricOverTimeGraph from './components/MetricOverTimeGraph';
|
||||
import StatusCodeTable from './components/StatusCodeTable';
|
||||
|
||||
function EndPointDetails({
|
||||
domainName,
|
||||
endPointName,
|
||||
setSelectedEndPointName,
|
||||
}: {
|
||||
domainName: string;
|
||||
endPointName: string;
|
||||
setSelectedEndPointName: (value: string) => void;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
||||
const query = currentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>({
|
||||
op: 'AND',
|
||||
items: [],
|
||||
});
|
||||
|
||||
const endPointDetailsQueryPayload = useMemo(
|
||||
() =>
|
||||
getEndPointDetailsQueryPayload(
|
||||
domainName,
|
||||
endPointName,
|
||||
Math.floor(minTime / 1e9),
|
||||
Math.floor(maxTime / 1e9),
|
||||
filters,
|
||||
),
|
||||
[domainName, endPointName, filters, minTime, maxTime],
|
||||
);
|
||||
|
||||
const endPointDetailsDataQueries = useQueries(
|
||||
endPointDetailsQueryPayload.map((payload, index) => ({
|
||||
queryKey: [`domain-endpoints-details-${index}`, payload, ENTITY_VERSION_V4],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
})),
|
||||
);
|
||||
|
||||
const [
|
||||
endPointMetricsDataQuery,
|
||||
endPointStatusCodeDataQuery,
|
||||
endPointRateOverTimeDataQuery,
|
||||
endPointLatencyOverTimeDataQuery,
|
||||
endPointDropDownDataQuery,
|
||||
endPointDependentServicesDataQuery,
|
||||
] = useMemo(
|
||||
() => [
|
||||
endPointDetailsDataQueries[0],
|
||||
endPointDetailsDataQueries[1],
|
||||
endPointDetailsDataQueries[2],
|
||||
endPointDetailsDataQueries[3],
|
||||
endPointDetailsDataQueries[4],
|
||||
endPointDetailsDataQueries[5],
|
||||
],
|
||||
[endPointDetailsDataQueries],
|
||||
);
|
||||
if (endPointName === '') {
|
||||
return (
|
||||
<EndPointDetailsZeroState
|
||||
endPointName={endPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
endPointDropDownDataQuery={endPointDropDownDataQuery}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="endpoint-details-container">
|
||||
<div className="endpoint-details-filters-container">
|
||||
<EndPointsDropDown
|
||||
selectedEndPointName={endPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
endPointDropDownDataQuery={endPointDropDownDataQuery}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={(searchFilters): void => setFilters(searchFilters)}
|
||||
placeholder="Search for filters..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EndPointMetrics endPointMetricsDataQuery={endPointMetricsDataQuery} />
|
||||
<DependentServices
|
||||
dependentServicesQuery={endPointDependentServicesDataQuery}
|
||||
/>
|
||||
<StatusCodeTable endPointStatusCodeDataQuery={endPointStatusCodeDataQuery} />
|
||||
<MetricOverTimeGraph
|
||||
metricOverTimeDataQuery={endPointRateOverTimeDataQuery}
|
||||
widgetInfoIndex={0}
|
||||
/>
|
||||
<MetricOverTimeGraph
|
||||
metricOverTimeDataQuery={endPointLatencyOverTimeDataQuery}
|
||||
widgetInfoIndex={1}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndPointDetails;
|
||||
@@ -0,0 +1,127 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Table } from 'antd';
|
||||
import { ColumnType } from 'antd/lib/table';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import {
|
||||
createFiltersForSelectedRowData,
|
||||
EndPointsTableRowData,
|
||||
formatEndPointsDataForTable,
|
||||
getEndPointsColumnsConfig,
|
||||
getEndPointsQueryPayload,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import LoadingContainer from 'container/InfraMonitoringK8s/LoadingContainer';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
|
||||
function ExpandedRow({
|
||||
domainName,
|
||||
selectedRowData,
|
||||
setSelectedEndPointName,
|
||||
setSelectedView,
|
||||
}: {
|
||||
domainName: string;
|
||||
selectedRowData: EndPointsTableRowData;
|
||||
setSelectedEndPointName: (name: string) => void;
|
||||
setSelectedView: (view: VIEWS) => void;
|
||||
}): JSX.Element {
|
||||
const nestedColumns = useMemo(() => getEndPointsColumnsConfig(false), []);
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const groupedByRowDataQueryPayload = useMemo(() => {
|
||||
if (!selectedRowData) return null;
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData);
|
||||
|
||||
const baseQueryPayload = getEndPointsQueryPayload(
|
||||
[],
|
||||
domainName,
|
||||
Math.floor(minTime / 1e9),
|
||||
Math.floor(maxTime / 1e9),
|
||||
);
|
||||
|
||||
return baseQueryPayload.map((currentQueryPayload) => ({
|
||||
...currentQueryPayload,
|
||||
query: {
|
||||
...currentQueryPayload.query,
|
||||
builder: {
|
||||
...currentQueryPayload.query.builder,
|
||||
queryData: currentQueryPayload.query.builder.queryData.map(
|
||||
(queryData) => ({
|
||||
...queryData,
|
||||
filters: {
|
||||
items: [...(queryData.filters?.items || []), ...filters.items],
|
||||
op: 'AND',
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
}));
|
||||
}, [domainName, minTime, maxTime, selectedRowData]);
|
||||
|
||||
const groupedByRowQueries = useQueries(
|
||||
groupedByRowDataQueryPayload
|
||||
? groupedByRowDataQueryPayload.map((payload) => ({
|
||||
queryKey: [
|
||||
`domain-endpoints-nested-expanded-row-${domainName}-${selectedRowData?.key}`,
|
||||
payload,
|
||||
ENTITY_VERSION_V4,
|
||||
'DOMAIN',
|
||||
selectedRowData?.key,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload && !!selectedRowData,
|
||||
}))
|
||||
: [],
|
||||
);
|
||||
|
||||
const groupedByRowQuery = groupedByRowQueries[0];
|
||||
return (
|
||||
<div className="expanded-table-container">
|
||||
{groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div className="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns as ColumnType<EndPointsTableRowData>[]}
|
||||
dataSource={
|
||||
groupedByRowQuery?.data
|
||||
? formatEndPointsDataForTable(
|
||||
groupedByRowQuery.data?.payload.data.result[0].table.rows,
|
||||
[],
|
||||
)
|
||||
: []
|
||||
}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
showHeader={false}
|
||||
loading={{
|
||||
spinning: groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedEndPointName(record.endpointName);
|
||||
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExpandedRow;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { getFormattedDependentServicesData } from 'container/ApiMonitoring/utils';
|
||||
import { UnfoldVertical } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
interface DependentServicesProps {
|
||||
dependentServicesQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||
}
|
||||
|
||||
// need to add a loading state
|
||||
|
||||
// discuss slice vs index based rendering
|
||||
function DependentServices({
|
||||
dependentServicesQuery,
|
||||
}: DependentServicesProps): JSX.Element {
|
||||
const { data } = dependentServicesQuery;
|
||||
|
||||
const [currentRenderCount, setCurrentRenderCount] = useState(0);
|
||||
|
||||
const dependentServicesData = useMemo(() => {
|
||||
const formattedDependentServicesData = getFormattedDependentServicesData(
|
||||
data?.payload?.data?.result[0].table.rows,
|
||||
);
|
||||
setCurrentRenderCount(Math.min(formattedDependentServicesData.length, 5));
|
||||
return formattedDependentServicesData;
|
||||
}, [data]);
|
||||
|
||||
const renderItems = useMemo(
|
||||
() => dependentServicesData.slice(0, currentRenderCount),
|
||||
[currentRenderCount, dependentServicesData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="top-attributes-content">
|
||||
<div className="top-attributes-title">
|
||||
<span className="title-wrapper">Dependent Services</span>
|
||||
</div>
|
||||
<div className="dependent-services-container">
|
||||
{renderItems.map((item) => (
|
||||
<div className="top-attributes-item" key={item.key}>
|
||||
<div className="top-attributes-item-progress">
|
||||
<div className="top-attributes-item-key">{item.serviceName}</div>
|
||||
<div className="top-attributes-item-count">{item.count}</div>
|
||||
<div
|
||||
className="top-attributes-item-progress-bar"
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="top-attributes-item-percentage">
|
||||
{item.percentage.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{currentRenderCount < dependentServicesData.length && (
|
||||
<div
|
||||
className="top-attributes-load-more"
|
||||
onClick={(): void => setCurrentRenderCount(dependentServicesData.length)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
setCurrentRenderCount(dependentServicesData.length);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<UnfoldVertical size={14} />
|
||||
Show more...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DependentServices;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
import EndPointsDropDown from './EndPointsDropDown';
|
||||
|
||||
function EndPointDetailsZeroState({
|
||||
endPointName,
|
||||
setSelectedEndPointName,
|
||||
endPointDropDownDataQuery,
|
||||
}: {
|
||||
endPointName: string;
|
||||
setSelectedEndPointName: (endPointName: string) => void;
|
||||
endPointDropDownDataQuery: UseQueryResult<SuccessResponse<any>>;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="end-point-details-zero-state-wrapper">
|
||||
<div className="end-point-details-zero-state-icon">Icon</div>
|
||||
<div className="end-point-details-zero-state-content-wrapper">
|
||||
<div className="end-point-details-zero-state-content">
|
||||
<div className="title">No endpoint selected yet</div>
|
||||
<div className="description">Select an endpoint to see the details</div>
|
||||
</div>
|
||||
<EndPointsDropDown
|
||||
selectedEndPointName={endPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
endPointDropDownDataQuery={endPointDropDownDataQuery}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndPointDetailsZeroState;
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import { getFormattedEndPointMetricsData } from 'container/ApiMonitoring/utils';
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
function EndPointMetrics({
|
||||
endPointMetricsDataQuery,
|
||||
}: {
|
||||
endPointMetricsDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||
}): JSX.Element {
|
||||
const { isLoading, isRefetching, isError, data } = endPointMetricsDataQuery;
|
||||
|
||||
const metricsData = useMemo(() => {
|
||||
if (isLoading || isRefetching || isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getFormattedEndPointMetricsData(
|
||||
data?.payload?.data?.result[0].table.rows,
|
||||
);
|
||||
}, [data?.payload?.data?.result, isLoading, isRefetching, isError]);
|
||||
|
||||
return (
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Rate
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
AVERAGE LATENCY
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
ERROR RATE
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
LAST USED
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={metricsData?.rate}>
|
||||
<span className="round-metric-tag">{metricsData?.rate}/sec</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={metricsData?.latency}>
|
||||
<span className="round-metric-tag">{metricsData?.latency}ms</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value error-rate">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={metricsData?.errorRate}>
|
||||
<Progress
|
||||
percent={Number((metricsData?.errorRate ?? 0 * 100).toFixed(1))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number(
|
||||
(metricsData?.errorRate ?? 0 * 100).toFixed(1),
|
||||
);
|
||||
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
|
||||
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={metricsData?.lastUsed}>{metricsData?.lastUsed}</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndPointMetrics;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Select } from 'antd';
|
||||
import { getFormattedEndPointDropDownData } from 'container/ApiMonitoring/utils';
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
function EndPointsDropDown({
|
||||
selectedEndPointName,
|
||||
setSelectedEndPointName,
|
||||
endPointDropDownDataQuery,
|
||||
}: {
|
||||
selectedEndPointName: string;
|
||||
setSelectedEndPointName: (value: string) => void;
|
||||
endPointDropDownDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||
}): JSX.Element {
|
||||
const { data, isLoading, isFetching } = endPointDropDownDataQuery;
|
||||
|
||||
const handleChange = (value: string): void => {
|
||||
setSelectedEndPointName(value);
|
||||
};
|
||||
|
||||
const formattedData = useMemo(
|
||||
() =>
|
||||
getFormattedEndPointDropDownData(data?.payload.data.result[0].table.rows),
|
||||
[data?.payload.data.result],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
placeholder="Select endpoint"
|
||||
loading={isLoading || isFetching}
|
||||
defaultValue={selectedEndPointName || ''}
|
||||
style={{ width: 120 }}
|
||||
onChange={handleChange}
|
||||
options={formattedData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndPointsDropDown;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Card, Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { apiWidgetInfo } from 'container/ApiMonitoring/utils';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Options } from 'uplot';
|
||||
|
||||
function MetricOverTimeGraph({
|
||||
metricOverTimeDataQuery,
|
||||
widgetInfoIndex,
|
||||
}: {
|
||||
metricOverTimeDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||
widgetInfoIndex: number;
|
||||
}): JSX.Element {
|
||||
const { data } = metricOverTimeDataQuery;
|
||||
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
|
||||
const chartData = useMemo(() => getUPlotChartData(data?.payload), [
|
||||
data?.payload,
|
||||
]);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
apiResponse: data?.payload,
|
||||
isDarkMode: true,
|
||||
dimensions,
|
||||
yAxisUnit: apiWidgetInfo[widgetInfoIndex].yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
minTimeScale: Math.floor(minTime / 1e9),
|
||||
maxTimeScale: Math.floor(maxTime / 1e9),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
}),
|
||||
[data?.payload, minTime, maxTime, widgetInfoIndex, dimensions],
|
||||
);
|
||||
|
||||
const renderCardContent = useCallback(
|
||||
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
|
||||
if (query.isLoading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
if (query.error) {
|
||||
const errorMessage =
|
||||
(query.error as Error)?.message || 'Something went wrong';
|
||||
return <div>{errorMessage}</div>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cx('chart-container', {
|
||||
'no-data-container':
|
||||
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||
})}
|
||||
>
|
||||
<Uplot options={options as Options} data={chartData} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[options, chartData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Text>{apiWidgetInfo[widgetInfoIndex].title}</Typography.Text>
|
||||
<Card bordered className="endpoint-details-card" ref={graphRef}>
|
||||
{renderCardContent(metricOverTimeDataQuery)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricOverTimeGraph;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Table } from 'antd';
|
||||
import {
|
||||
endPointStatusCodeColumns,
|
||||
getFormattedEndPointStatusCodeData,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
function StatusCodeTable({
|
||||
endPointStatusCodeDataQuery,
|
||||
}: {
|
||||
endPointStatusCodeDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||
}): JSX.Element {
|
||||
const { isLoading, isRefetching, isError, data } = endPointStatusCodeDataQuery;
|
||||
|
||||
const statusCodeData = useMemo(() => {
|
||||
if (isLoading || isRefetching || isError) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getFormattedEndPointStatusCodeData(
|
||||
data?.payload?.data?.result[0].table.rows,
|
||||
);
|
||||
}, [data?.payload?.data?.result, isLoading, isRefetching, isError]);
|
||||
|
||||
return (
|
||||
<div className="status-code-table-container">
|
||||
<Table
|
||||
loading={isLoading || isRefetching}
|
||||
dataSource={statusCodeData || []}
|
||||
columns={endPointStatusCodeColumns}
|
||||
pagination={false}
|
||||
rowClassName={(_, index): string =>
|
||||
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusCodeTable;
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum VIEWS {
|
||||
ALL_ENDPOINTS = 'all_endpoints',
|
||||
ENDPOINT_DETAILS = 'endpoint_details',
|
||||
}
|
||||
|
||||
export const VIEW_TYPES = {
|
||||
ALL_ENDPOINTS: VIEWS.ALL_ENDPOINTS,
|
||||
ENDPOINT_DETAILS: VIEWS.ENDPOINT_DETAILS,
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
import '../Explorer.styles.scss';
|
||||
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Table, Typography } from 'antd';
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { HandleChangeQueryData } from 'types/common/operations.types';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
columnsConfig,
|
||||
formatDataForTable,
|
||||
hardcodedAttributeKeys,
|
||||
} from '../../utils';
|
||||
import DomainDetails from './DomainDetails/DomainDetails';
|
||||
|
||||
function DomainList({
|
||||
query,
|
||||
showIP,
|
||||
handleChangeQueryData,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
showIP: boolean;
|
||||
handleChangeQueryData: HandleChangeQueryData;
|
||||
}): JSX.Element {
|
||||
const [selectedDomainData, setSelectedDomainData] = useState<any>(null);
|
||||
// const [selectedDomainIndex, setSelectedDomainIndex] = useState<
|
||||
// number | undefined
|
||||
// >(undefined);
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const fetchApiOverview = async (): Promise<
|
||||
SuccessResponse<any> | ErrorResponse
|
||||
> => {
|
||||
const requestBody = {
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
show_ip: showIP,
|
||||
filters: {
|
||||
op: 'AND',
|
||||
items: query?.filters.items,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
'/third-party-apis/overview/list',
|
||||
requestBody,
|
||||
);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
const { data, isLoading, isFetching } = useQuery(
|
||||
['apiOverview', minTime, maxTime, query, showIP],
|
||||
fetchApiOverview,
|
||||
);
|
||||
|
||||
const formattedDataForTable = useMemo(
|
||||
() => formatDataForTable(data?.payload?.data?.result[0]?.table?.rows),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={cx('api-module-right-section')}>
|
||||
<div className={cx('api-monitoring-list-header')}>
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={(searchFilters): void =>
|
||||
handleChangeQueryData('filters', searchFilters)
|
||||
}
|
||||
placeholder="Search filters..."
|
||||
hardcodedAttributeKeys={hardcodedAttributeKeys}
|
||||
/>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={false}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
className={cx('api-monitoring-domain-list-table')}
|
||||
dataSource={isFetching || isLoading ? [] : formattedDataForTable}
|
||||
columns={columnsConfig}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText:
|
||||
isFetching || isLoading ? null : (
|
||||
<div className="no-filtered-domains-message-container">
|
||||
<div className="no-filtered-domains-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-domains-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
// setSelectedDomainIndex(index);
|
||||
setSelectedDomainData(record); // TODO: update the selected domain data based on index later to support navigation
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
{selectedDomainData && (
|
||||
<DomainDetails
|
||||
domainData={selectedDomainData}
|
||||
handleClose={(): void => {
|
||||
setSelectedDomainData(null);
|
||||
// setSelectedDomainIndex(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default DomainList;
|
||||
@@ -0,0 +1,199 @@
|
||||
.api-monitoring-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
.api-quick-filter-left-section {
|
||||
width: 0%;
|
||||
flex-shrink: 0;
|
||||
|
||||
.api-quick-filters-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.api-monitoring-list-header {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.query-builder-search-v2 {
|
||||
min-width: 80%;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.api-monitoring-domain-list-table {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
background: var(--bg-ink-500);
|
||||
border-bottom: none;
|
||||
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
/* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.domain-list-name-col-header) {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--bg-vanilla-100);
|
||||
background: var(--bg-ink-500);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.domain-list-name-col-value) {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.hostname-column-value {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: 'Geist Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.status-cell {
|
||||
.active-tag {
|
||||
color: var(--bg-forest-500);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
.ant-progress-bg {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.round-metric-tag {
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: fit-content;
|
||||
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--Slate-400, #1d212d);
|
||||
background: var(--Slate-500, #161922);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-cell:first-child {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(2) {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(n + 3) {
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.column-header-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-table-thead
|
||||
> tr
|
||||
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-empty-normal {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
.api-quick-filter-left-section {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-filtered-domains-message-container {
|
||||
height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.no-filtered-domains-message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
width: fit-content;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.no-filtered-domains-message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
91
frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx
Normal file
91
frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import './Explorer.styles.scss';
|
||||
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Switch, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { ApiMonitoringQuickFiltersConfig } from '../utils';
|
||||
import DomainList from './Domains/DomainList';
|
||||
|
||||
function Explorer(): JSX.Element {
|
||||
const [showIP, setShowIP] = useState<boolean>(true);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[currentQuery],
|
||||
);
|
||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
||||
<section className={cx('api-quick-filter-left-section')}>
|
||||
<div className={cx('api-quick-filters-header')}>
|
||||
<FilterOutlined />
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('api-quick-filters-header')}>
|
||||
<Typography.Text>Show IP addresses</Typography.Text>
|
||||
<Switch
|
||||
size="small"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
checked={showIP}
|
||||
onClick={(): void => {
|
||||
setShowIP((showIP) => !showIP);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
config={ApiMonitoringQuickFiltersConfig}
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
onFilterChange={(query: Query): void =>
|
||||
handleChangeQueryData('filters', query.builder.queryData[0].filters)
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
<DomainList
|
||||
query={query}
|
||||
showIP={showIP}
|
||||
handleChangeQueryData={handleChangeQueryData}
|
||||
/>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default Explorer;
|
||||
1769
frontend/src/container/ApiMonitoring/utils.tsx
Normal file
1769
frontend/src/container/ApiMonitoring/utils.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -307,6 +307,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
routeKey === 'LOGS_PIPELINES' ||
|
||||
routeKey === 'LOGS_SAVE_VIEWS';
|
||||
|
||||
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
|
||||
|
||||
const isTracesView = (): boolean =>
|
||||
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||
|
||||
@@ -556,7 +558,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isAlertOverview() ||
|
||||
isMessagingQueues() ||
|
||||
isCloudIntegrationPage() ||
|
||||
isInfraMonitoring()
|
||||
isInfraMonitoring() ||
|
||||
isApiMonitoringView()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
|
||||
|
||||
@@ -186,16 +186,14 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
|
||||
const sortedDashboards = dashboards.sort(
|
||||
(a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
setDashboards(sortedDashboards);
|
||||
};
|
||||
|
||||
const sortDashboardsByUpdatedAt = (dashboards: Dashboard[]): void => {
|
||||
const sortedDashboards = dashboards.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(),
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
);
|
||||
setDashboards(sortedDashboards);
|
||||
};
|
||||
@@ -260,16 +258,16 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
const data: Data[] =
|
||||
dashboards?.map((e) => ({
|
||||
createdAt: e.created_at,
|
||||
createdAt: e.createdAt,
|
||||
description: e.data.description || '',
|
||||
id: e.uuid,
|
||||
lastUpdatedTime: e.updated_at,
|
||||
lastUpdatedTime: e.updatedAt,
|
||||
name: e.data.title,
|
||||
tags: e.data.tags || [],
|
||||
key: e.uuid,
|
||||
createdBy: e.created_by,
|
||||
createdBy: e.createdBy,
|
||||
isLocked: !!e.isLocked || false,
|
||||
lastUpdatedBy: e.updated_by,
|
||||
lastUpdatedBy: e.updatedBy,
|
||||
image: e.data.image || Base64Icons[0],
|
||||
variables: e.data.variables,
|
||||
widgets: e.data.widgets,
|
||||
@@ -978,10 +976,10 @@ function DashboardsList(): JSX.Element {
|
||||
{visibleColumns.createdBy && (
|
||||
<div className="user">
|
||||
<Typography.Text className="user-tag">
|
||||
{dashboards?.[0]?.created_by?.substring(0, 1).toUpperCase()}
|
||||
{dashboards?.[0]?.createdBy?.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="dashboard-created-by">
|
||||
{dashboards?.[0]?.created_by}
|
||||
{dashboards?.[0]?.createdBy}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
@@ -990,16 +988,16 @@ function DashboardsList(): JSX.Element {
|
||||
{visibleColumns.updatedAt && (
|
||||
<Typography.Text className="formatted-time">
|
||||
<CalendarClock size={14} />
|
||||
{onLastUpdated(dashboards?.[0]?.updated_at || '')}
|
||||
{onLastUpdated(dashboards?.[0]?.updatedAt || '')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{visibleColumns.updatedBy && (
|
||||
<div className="user">
|
||||
<Typography.Text className="user-tag">
|
||||
{dashboards?.[0]?.updated_by?.substring(0, 1).toUpperCase()}
|
||||
{dashboards?.[0]?.updatedBy?.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="dashboard-created-by">
|
||||
{dashboards?.[0]?.updated_by}
|
||||
{dashboards?.[0]?.updatedBy}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -140,7 +140,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
let isAuthor = false;
|
||||
|
||||
if (selectedDashboard && user && user.email) {
|
||||
isAuthor = selectedDashboard?.created_by === user?.email;
|
||||
isAuthor = selectedDashboard?.createdBy === user?.email;
|
||||
}
|
||||
|
||||
let permissions: ComponentTypes[] = ['add_panel'];
|
||||
@@ -152,7 +152,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const userRole: ROLES | null =
|
||||
selectedDashboard?.created_by === user?.email
|
||||
selectedDashboard?.createdBy === user?.email
|
||||
? (USER_ROLES.AUTHOR as ROLES)
|
||||
: user.role;
|
||||
|
||||
@@ -364,14 +364,14 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
{(isAuthor || user.role === USER_ROLES.ADMIN) && (
|
||||
<Tooltip
|
||||
title={
|
||||
selectedDashboard?.created_by === 'integration' &&
|
||||
selectedDashboard?.createdBy === 'integration' &&
|
||||
'Dashboards created by integrations cannot be unlocked'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<LockKeyhole size={14} />}
|
||||
disabled={selectedDashboard?.created_by === 'integration'}
|
||||
disabled={selectedDashboard?.createdBy === 'integration'}
|
||||
onClick={handleLockDashboardToggle}
|
||||
data-testid="lock-unlock-dashboard"
|
||||
>
|
||||
@@ -443,7 +443,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
</section>
|
||||
<section className="delete-dashboard">
|
||||
<DeleteButton
|
||||
createdBy={selectedDashboard?.created_by || ''}
|
||||
createdBy={selectedDashboard?.createdBy || ''}
|
||||
name={selectedDashboard?.data.title || ''}
|
||||
id={String(selectedDashboard?.uuid) || ''}
|
||||
isLocked={isDashboardLocked}
|
||||
|
||||
@@ -86,6 +86,7 @@ interface QueryBuilderSearchV2Props {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
suffixIcon?: React.ReactNode;
|
||||
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@@ -118,6 +119,7 @@ function QueryBuilderSearchV2(
|
||||
className,
|
||||
suffixIcon,
|
||||
whereClauseConfig,
|
||||
hardcodedAttributeKeys,
|
||||
} = props;
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
@@ -232,7 +234,7 @@ function QueryBuilderSearchV2(
|
||||
},
|
||||
{
|
||||
queryKey: [searchParams],
|
||||
enabled: isQueryEnabled && !isLogsDataSource,
|
||||
enabled: isQueryEnabled && !isLogsDataSource && !hardcodedAttributeKeys,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -673,6 +675,18 @@ function QueryBuilderSearchV2(
|
||||
value: key,
|
||||
})) || []),
|
||||
]);
|
||||
} else if (hardcodedAttributeKeys) {
|
||||
const filteredKeys = hardcodedAttributeKeys.filter((key) =>
|
||||
key.key
|
||||
.toLowerCase()
|
||||
.includes((searchValue?.split(' ')[0] || '').toLowerCase()),
|
||||
);
|
||||
setDropdownOptions(
|
||||
filteredKeys.map((key) => ({
|
||||
label: key.key,
|
||||
value: key,
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
setDropdownOptions(
|
||||
data?.payload?.attributeKeys?.map((key) => ({
|
||||
@@ -749,6 +763,7 @@ function QueryBuilderSearchV2(
|
||||
);
|
||||
}
|
||||
}, [
|
||||
hardcodedAttributeKeys,
|
||||
attributeValues?.payload,
|
||||
currentFilterItem?.key?.dataType,
|
||||
currentState,
|
||||
@@ -981,6 +996,7 @@ QueryBuilderSearchV2.defaultProps = {
|
||||
className: '',
|
||||
suffixIcon: null,
|
||||
whereClauseConfig: {},
|
||||
hardcodedAttributeKeys: undefined,
|
||||
};
|
||||
|
||||
export default QueryBuilderSearchV2;
|
||||
|
||||
@@ -3,6 +3,7 @@ import ROUTES from 'constants/routes';
|
||||
import {
|
||||
BarChart2,
|
||||
BellDot,
|
||||
Binoculars,
|
||||
Boxes,
|
||||
BugIcon,
|
||||
Cloudy,
|
||||
@@ -111,6 +112,11 @@ const menuItems: SidebarItem[] = [
|
||||
label: 'Messaging Queues',
|
||||
icon: <ListMinus size={16} />,
|
||||
},
|
||||
{
|
||||
key: ROUTES.API_MONITORING,
|
||||
label: 'API Monitoring',
|
||||
icon: <Binoculars size={16} />,
|
||||
},
|
||||
{
|
||||
key: ROUTES.LIST_ALL_ALERT,
|
||||
label: 'Alerts',
|
||||
|
||||
@@ -223,6 +223,7 @@ export const routesToSkip = [
|
||||
ROUTES.METRICS_EXPLORER,
|
||||
ROUTES.METRICS_EXPLORER_EXPLORER,
|
||||
ROUTES.METRICS_EXPLORER_VIEWS,
|
||||
ROUTES.API_MONITORING,
|
||||
ROUTES.CHANNELS_NEW,
|
||||
ROUTES.CHANNELS_EDIT,
|
||||
];
|
||||
|
||||
@@ -11,7 +11,7 @@ export const useUpdateDashboard = (): UseUpdateDashboard => {
|
||||
return useMutation(update, {
|
||||
onSuccess: (data) => {
|
||||
if (data.payload) {
|
||||
updatedTimeRef.current = dayjs(data.payload.updated_at);
|
||||
updatedTimeRef.current = dayjs(data.payload.updatedAt);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -102,4 +102,5 @@ export interface GetQueryResultsProps {
|
||||
};
|
||||
start?: number;
|
||||
end?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ export const dashboardSuccessResponse = {
|
||||
{
|
||||
id: 1,
|
||||
uuid: '1',
|
||||
created_at: '2022-11-16T13:29:47.064874419Z',
|
||||
created_by: null,
|
||||
updated_at: '2024-05-21T06:41:30.546630961Z',
|
||||
updated_by: 'thor@avengers.io',
|
||||
createdAt: '2022-11-16T13:29:47.064874419Z',
|
||||
createdBy: null,
|
||||
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
||||
updatedBy: 'thor@avengers.io',
|
||||
isLocked: 0,
|
||||
data: {
|
||||
collapsableRowsMigrated: true,
|
||||
@@ -25,10 +25,10 @@ export const dashboardSuccessResponse = {
|
||||
{
|
||||
id: 2,
|
||||
uuid: '2',
|
||||
created_at: '2022-11-16T13:20:47.064874419Z',
|
||||
created_by: null,
|
||||
updated_at: '2024-05-21T06:42:30.546630961Z',
|
||||
updated_by: 'captain-america@avengers.io',
|
||||
createdAt: '2022-11-16T13:20:47.064874419Z',
|
||||
createdBy: null,
|
||||
updatedAt: '2024-05-21T06:42:30.546630961Z',
|
||||
updatedBy: 'captain-america@avengers.io',
|
||||
isLocked: 0,
|
||||
data: {
|
||||
collapsableRowsMigrated: true,
|
||||
@@ -55,10 +55,10 @@ export const getDashboardById = {
|
||||
data: {
|
||||
id: 1,
|
||||
uuid: '1',
|
||||
created_at: '2022-11-16T13:29:47.064874419Z',
|
||||
created_by: 'integration',
|
||||
updated_at: '2024-05-21T06:41:30.546630961Z',
|
||||
updated_by: 'thor@avengers.io',
|
||||
createdAt: '2022-11-16T13:29:47.064874419Z',
|
||||
createdBy: 'integration',
|
||||
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
||||
updatedBy: 'thor@avengers.io',
|
||||
isLocked: true,
|
||||
data: {
|
||||
collapsableRowsMigrated: true,
|
||||
@@ -80,10 +80,10 @@ export const getNonIntegrationDashboardById = {
|
||||
data: {
|
||||
id: 1,
|
||||
uuid: '1',
|
||||
created_at: '2022-11-16T13:29:47.064874419Z',
|
||||
created_by: 'thor',
|
||||
updated_at: '2024-05-21T06:41:30.546630961Z',
|
||||
updated_by: 'thor@avengers.io',
|
||||
createdAt: '2022-11-16T13:29:47.064874419Z',
|
||||
createdBy: 'thor',
|
||||
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
||||
updatedBy: 'thor@avengers.io',
|
||||
isLocked: true,
|
||||
data: {
|
||||
collapsableRowsMigrated: true,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
.api-monitoring-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
.ant-tabs {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 0px;
|
||||
|
||||
&::before {
|
||||
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
display: flex;
|
||||
|
||||
.ant-tabs-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.api-monitoring-page {
|
||||
.ant-tabs-nav {
|
||||
&::before {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
frontend/src/pages/ApiMonitoring/ApiMonitoringPage.tsx
Normal file
22
frontend/src/pages/ApiMonitoring/ApiMonitoringPage.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import './ApiMonitoringPage.styles.scss';
|
||||
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import history from 'lib/history';
|
||||
import { useLocation } from 'react-use';
|
||||
|
||||
import { Explorer } from './constants';
|
||||
|
||||
function ApiMonitoringPage(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const routes: TabRoutes[] = [Explorer];
|
||||
|
||||
return (
|
||||
<div className="api-monitoring-page">
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiMonitoringPage;
|
||||
15
frontend/src/pages/ApiMonitoring/constants.tsx
Normal file
15
frontend/src/pages/ApiMonitoring/constants.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import ROUTES from 'constants/routes';
|
||||
import ExplorerPage from 'container/ApiMonitoring/Explorer/Explorer';
|
||||
import { Compass } from 'lucide-react';
|
||||
|
||||
export const Explorer: TabRoutes = {
|
||||
Component: ExplorerPage,
|
||||
name: (
|
||||
<div className="tab-item">
|
||||
<Compass size={16} /> Explorer
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.API_MONITORING,
|
||||
key: ROUTES.API_MONITORING,
|
||||
};
|
||||
3
frontend/src/pages/ApiMonitoring/index.ts
Normal file
3
frontend/src/pages/ApiMonitoring/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ApiMonitoring from './ApiMonitoringPage';
|
||||
|
||||
export default ApiMonitoring;
|
||||
@@ -273,7 +273,7 @@ export function DashboardProvider({
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: (data) => {
|
||||
const updatedDashboardData = transformDashboardVariables(data);
|
||||
const updatedDate = dayjs(updatedDashboardData.updated_at);
|
||||
const updatedDate = dayjs(updatedDashboardData.updatedAt);
|
||||
|
||||
setIsDashboardLocked(updatedDashboardData?.isLocked || false);
|
||||
|
||||
@@ -321,7 +321,7 @@ export function DashboardProvider({
|
||||
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData.updated_at);
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData.updatedAt);
|
||||
|
||||
setLayouts(
|
||||
sortLayout(getUpdatedLayout(updatedDashboardData.data.layout)),
|
||||
@@ -334,7 +334,7 @@ export function DashboardProvider({
|
||||
modalRef.current = modal;
|
||||
} else {
|
||||
// normal flow
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData.updated_at);
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData.updatedAt);
|
||||
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
|
||||
@@ -46,10 +46,10 @@ export interface IDashboardVariable {
|
||||
export interface Dashboard {
|
||||
id: number;
|
||||
uuid: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
data: DashboardData;
|
||||
isLocked?: boolean;
|
||||
}
|
||||
|
||||
@@ -114,4 +114,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
METRICS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
METRICS_EXPLORER_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
METRICS_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
API_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
};
|
||||
|
||||
@@ -11111,10 +11111,10 @@ lru-cache@^6.0.0:
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
|
||||
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
|
||||
|
||||
lucide-react@0.379.0:
|
||||
version "0.379.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.379.0.tgz#29e34eeffae7fb241b64b09868cbe3ab888ef7cc"
|
||||
integrity sha512-KcdeVPqmhRldldAAgptb8FjIunM2x2Zy26ZBh1RsEUcdLIvsEmbcw7KpzFYUy5BbpGeWhPu9Z9J5YXfStiXwhg==
|
||||
lucide-react@0.427.0:
|
||||
version "0.427.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.427.0.tgz#e06974514bbd591049f9d736b3d3ae99d4ede8c9"
|
||||
integrity sha512-lv9s6c5BDF/ccuA0EgTdskTxIe11qpwBDmzRZHJAKtp8LTewAvDvOM+pTES9IpbBuTqkjiMhOmGpJ/CB+mKjFw==
|
||||
|
||||
lz-string@^1.4.4:
|
||||
version "1.5.0"
|
||||
|
||||
@@ -218,7 +218,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
|
||||
config := alertmanagerConfig.AlertmanagerConfig()
|
||||
|
||||
var err error
|
||||
server.tmpl, err = template.FromGlobs(config.Templates)
|
||||
server.tmpl, err = alertmanagertypes.FromGlobs(config.Templates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ var (
|
||||
CodeMethodNotAllowed = code{"method_not_allowed"}
|
||||
CodeAlreadyExists = code{"already_exists"}
|
||||
CodeUnauthenticated = code{"unauthenticated"}
|
||||
CodeForbidden = code{"forbidden"}
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -8,6 +8,7 @@ var (
|
||||
TypeMethodNotAllowed = typ{"method-not-allowed"}
|
||||
TypeAlreadyExists = typ{"already-exists"}
|
||||
TypeUnauthenticated = typ{"unauthenticated"}
|
||||
TypeForbidden = typ{"forbidden"}
|
||||
)
|
||||
|
||||
// Defines custom error types
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
@@ -493,9 +493,9 @@ func (c *Controller) UpdateServiceConfig(
|
||||
// All dashboards that are available based on cloud integrations configuration
|
||||
// across all cloud providers
|
||||
func (c *Controller) AvailableDashboards(ctx context.Context) (
|
||||
[]dashboards.Dashboard, *model.ApiError,
|
||||
[]types.Dashboard, *model.ApiError,
|
||||
) {
|
||||
allDashboards := []dashboards.Dashboard{}
|
||||
allDashboards := []types.Dashboard{}
|
||||
|
||||
for _, provider := range []string{"aws"} {
|
||||
providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, provider)
|
||||
@@ -513,7 +513,7 @@ func (c *Controller) AvailableDashboards(ctx context.Context) (
|
||||
|
||||
func (c *Controller) AvailableDashboardsForCloudProvider(
|
||||
ctx context.Context, cloudProvider string,
|
||||
) ([]dashboards.Dashboard, *model.ApiError) {
|
||||
) ([]types.Dashboard, *model.ApiError) {
|
||||
|
||||
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, cloudProvider)
|
||||
if apiErr != nil {
|
||||
@@ -545,21 +545,25 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
svcDashboards := []dashboards.Dashboard{}
|
||||
svcDashboards := []types.Dashboard{}
|
||||
for _, svc := range allServices {
|
||||
serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id]
|
||||
if serviceDashboardsCreatedAt != nil {
|
||||
for _, d := range svc.Assets.Dashboards {
|
||||
isLocked := 1
|
||||
author := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
svcDashboards = append(svcDashboards, dashboards.Dashboard{
|
||||
Uuid: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
|
||||
Locked: &isLocked,
|
||||
Data: *d.Definition,
|
||||
CreatedAt: *serviceDashboardsCreatedAt,
|
||||
CreateBy: &author,
|
||||
UpdatedAt: *serviceDashboardsCreatedAt,
|
||||
UpdateBy: &author,
|
||||
svcDashboards = append(svcDashboards, types.Dashboard{
|
||||
UUID: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
|
||||
Locked: &isLocked,
|
||||
Data: *d.Definition,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: *serviceDashboardsCreatedAt,
|
||||
UpdatedAt: *serviceDashboardsCreatedAt,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: author,
|
||||
UpdatedBy: author,
|
||||
},
|
||||
})
|
||||
}
|
||||
servicesWithAvailableMetrics[svc.Id] = nil
|
||||
@@ -571,7 +575,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
|
||||
func (c *Controller) GetDashboardById(
|
||||
ctx context.Context,
|
||||
dashboardUuid string,
|
||||
) (*dashboards.Dashboard, *model.ApiError) {
|
||||
) (*types.Dashboard, *model.ApiError) {
|
||||
cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
@@ -585,7 +589,7 @@ func (c *Controller) GetDashboardById(
|
||||
}
|
||||
|
||||
for _, d := range allDashboards {
|
||||
if d.Uuid == dashboardUuid {
|
||||
if d.UUID == dashboardUuid {
|
||||
return &d, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
)
|
||||
|
||||
// Represents a cloud provider account for cloud integrations
|
||||
@@ -187,12 +187,12 @@ type CloudServiceAssets struct {
|
||||
}
|
||||
|
||||
type CloudServiceDashboard struct {
|
||||
Id string `json:"id"`
|
||||
Url string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Definition *dashboards.Data `json:"definition,omitempty"`
|
||||
Id string `json:"id"`
|
||||
Url string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Definition *types.DashboardData `json:"definition,omitempty"`
|
||||
}
|
||||
|
||||
type SupportedSignals struct {
|
||||
|
||||
@@ -2,7 +2,6 @@ package dashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
@@ -11,18 +10,17 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"github.com/uptrace/bun"
|
||||
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// This time the global variable is unexported.
|
||||
var db *sqlx.DB
|
||||
var db *bun.DB
|
||||
|
||||
// User for mapping job,instance from grafana
|
||||
var (
|
||||
@@ -35,96 +33,43 @@ var (
|
||||
)
|
||||
|
||||
// InitDB sets up setting up the connection pool global variable.
|
||||
func InitDB(inputDB *sqlx.DB) error {
|
||||
func InitDB(inputDB *bun.DB) error {
|
||||
db = inputDB
|
||||
telemetry.GetInstance().SetDashboardsInfoCallback(GetDashboardsInfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Dashboard struct {
|
||||
Id int `json:"id" db:"id"`
|
||||
Uuid string `json:"uuid" db:"uuid"`
|
||||
Slug string `json:"-" db:"-"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
CreateBy *string `json:"created_by" db:"created_by"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
UpdateBy *string `json:"updated_by" db:"updated_by"`
|
||||
Title string `json:"-" db:"-"`
|
||||
Data Data `json:"data" db:"data"`
|
||||
Locked *int `json:"isLocked" db:"locked"`
|
||||
}
|
||||
|
||||
type Data map[string]interface{}
|
||||
|
||||
// func (c *Data) Value() (driver.Value, error) {
|
||||
// if c != nil {
|
||||
// b, err := json.Marshal(c)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return string(b), nil
|
||||
// }
|
||||
// return nil, nil
|
||||
// }
|
||||
|
||||
func (c *Data) Scan(src interface{}) error {
|
||||
var data []byte
|
||||
if b, ok := src.([]byte); ok {
|
||||
data = b
|
||||
} else if s, ok := src.(string); ok {
|
||||
data = []byte(s)
|
||||
}
|
||||
return json.Unmarshal(data, c)
|
||||
}
|
||||
|
||||
// CreateDashboard creates a new dashboard
|
||||
func CreateDashboard(ctx context.Context, data map[string]interface{}, fm interfaces.FeatureLookup) (*Dashboard, *model.ApiError) {
|
||||
dash := &Dashboard{
|
||||
func CreateDashboard(ctx context.Context, orgID string, email string, data map[string]interface{}, fm interfaces.FeatureLookup) (*types.Dashboard, *model.ApiError) {
|
||||
dash := &types.Dashboard{
|
||||
Data: data,
|
||||
}
|
||||
var userEmail string
|
||||
if user := common.GetUserFromContext(ctx); user != nil {
|
||||
userEmail = user.Email
|
||||
}
|
||||
|
||||
dash.OrgID = orgID
|
||||
dash.CreatedAt = time.Now()
|
||||
dash.CreateBy = &userEmail
|
||||
dash.CreatedBy = email
|
||||
dash.UpdatedAt = time.Now()
|
||||
dash.UpdateBy = &userEmail
|
||||
dash.UpdatedBy = email
|
||||
dash.UpdateSlug()
|
||||
dash.Uuid = uuid.New().String()
|
||||
dash.UUID = uuid.New().String()
|
||||
if data["uuid"] != nil {
|
||||
dash.Uuid = data["uuid"].(string)
|
||||
dash.UUID = data["uuid"].(string)
|
||||
}
|
||||
|
||||
mapData, err := json.Marshal(dash.Data)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in marshalling data field in dashboard: ", zap.Any("dashboard", dash), zap.Error(err))
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
result, err := db.Exec("INSERT INTO dashboards (uuid, created_at, created_by, updated_at, updated_by, data) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
dash.Uuid, dash.CreatedAt, userEmail, dash.UpdatedAt, userEmail, mapData)
|
||||
|
||||
err := db.NewInsert().Model(dash).Returning("id").Scan(ctx, &dash.ID)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in inserting dashboard data: ", zap.Any("dashboard", dash), zap.Error(err))
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
lastInsertId, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
dash.Id = int(lastInsertId)
|
||||
|
||||
return dash, nil
|
||||
}
|
||||
|
||||
func GetDashboards(ctx context.Context) ([]Dashboard, *model.ApiError) {
|
||||
func GetDashboards(ctx context.Context, orgID string) ([]types.Dashboard, *model.ApiError) {
|
||||
dashboards := []types.Dashboard{}
|
||||
|
||||
dashboards := []Dashboard{}
|
||||
query := `SELECT * FROM dashboards`
|
||||
|
||||
err := db.Select(&dashboards, query)
|
||||
err := db.NewSelect().Model(&dashboards).Where("org_id = ?", orgID).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
@@ -132,23 +77,19 @@ func GetDashboards(ctx context.Context) ([]Dashboard, *model.ApiError) {
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
func DeleteDashboard(ctx context.Context, uuid string, fm interfaces.FeatureLookup) *model.ApiError {
|
||||
func DeleteDashboard(ctx context.Context, orgID, uuid string, fm interfaces.FeatureLookup) *model.ApiError {
|
||||
|
||||
dashboard, dErr := GetDashboard(ctx, uuid)
|
||||
dashboard, dErr := GetDashboard(ctx, orgID, uuid)
|
||||
if dErr != nil {
|
||||
zap.L().Error("Error in getting dashboard: ", zap.String("uuid", uuid), zap.Any("error", dErr))
|
||||
return dErr
|
||||
}
|
||||
|
||||
if user := common.GetUserFromContext(ctx); user != nil {
|
||||
if dashboard.Locked != nil && *dashboard.Locked == 1 {
|
||||
return model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to delete it"))
|
||||
}
|
||||
if dashboard.Locked != nil && *dashboard.Locked == 1 {
|
||||
return model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to delete it"))
|
||||
}
|
||||
|
||||
query := `DELETE FROM dashboards WHERE uuid=?`
|
||||
|
||||
result, err := db.Exec(query, uuid)
|
||||
result, err := db.NewDelete().Model(&types.Dashboard{}).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Exec(ctx)
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
@@ -164,12 +105,10 @@ func DeleteDashboard(ctx context.Context, uuid string, fm interfaces.FeatureLook
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDashboard(ctx context.Context, uuid string) (*Dashboard, *model.ApiError) {
|
||||
func GetDashboard(ctx context.Context, orgID, uuid string) (*types.Dashboard, *model.ApiError) {
|
||||
|
||||
dashboard := Dashboard{}
|
||||
query := `SELECT * FROM dashboards WHERE uuid=?`
|
||||
|
||||
err := db.Get(&dashboard, query, uuid)
|
||||
dashboard := types.Dashboard{}
|
||||
err := db.NewSelect().Model(&dashboard).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no dashboard found with uuid: %s", uuid)}
|
||||
}
|
||||
@@ -177,7 +116,7 @@ func GetDashboard(ctx context.Context, uuid string) (*Dashboard, *model.ApiError
|
||||
return &dashboard, nil
|
||||
}
|
||||
|
||||
func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface{}, fm interfaces.FeatureLookup) (*Dashboard, *model.ApiError) {
|
||||
func UpdateDashboard(ctx context.Context, orgID, userEmail, uuid string, data map[string]interface{}, fm interfaces.FeatureLookup) (*types.Dashboard, *model.ApiError) {
|
||||
|
||||
mapData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
@@ -185,17 +124,13 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface
|
||||
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
}
|
||||
|
||||
dashboard, apiErr := GetDashboard(ctx, uuid)
|
||||
dashboard, apiErr := GetDashboard(ctx, orgID, uuid)
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
var userEmail string
|
||||
if user := common.GetUserFromContext(ctx); user != nil {
|
||||
userEmail = user.Email
|
||||
if dashboard.Locked != nil && *dashboard.Locked == 1 {
|
||||
return nil, model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to edit it"))
|
||||
}
|
||||
if dashboard.Locked != nil && *dashboard.Locked == 1 {
|
||||
return nil, model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to edit it"))
|
||||
}
|
||||
|
||||
// if the total count of panels has reduced by more than 1,
|
||||
@@ -210,11 +145,10 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface
|
||||
}
|
||||
|
||||
dashboard.UpdatedAt = time.Now()
|
||||
dashboard.UpdateBy = &userEmail
|
||||
dashboard.UpdatedBy = userEmail
|
||||
dashboard.Data = data
|
||||
|
||||
_, err = db.Exec("UPDATE dashboards SET updated_at=$1, updated_by=$2, data=$3 WHERE uuid=$4;",
|
||||
dashboard.UpdatedAt, userEmail, mapData, dashboard.Uuid)
|
||||
_, err = db.NewUpdate().Model(dashboard).Set("updated_at = ?", dashboard.UpdatedAt).Set("updated_by = ?", userEmail).Set("data = ?", mapData).Where("uuid = ?", dashboard.UUID).Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("Error in inserting dashboard data", zap.Any("data", data), zap.Error(err))
|
||||
@@ -223,16 +157,20 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
func LockUnlockDashboard(ctx context.Context, uuid string, lock bool) *model.ApiError {
|
||||
var query string
|
||||
if lock {
|
||||
query = `UPDATE dashboards SET locked=1 WHERE uuid=?;`
|
||||
} else {
|
||||
query = `UPDATE dashboards SET locked=0 WHERE uuid=?;`
|
||||
func LockUnlockDashboard(ctx context.Context, orgID, uuid string, lock bool) *model.ApiError {
|
||||
dashboard, apiErr := GetDashboard(ctx, orgID, uuid)
|
||||
if apiErr != nil {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
_, err := db.Exec(query, uuid)
|
||||
var lockValue int
|
||||
if lock {
|
||||
lockValue = 1
|
||||
} else {
|
||||
lockValue = 0
|
||||
}
|
||||
|
||||
_, err := db.NewUpdate().Model(dashboard).Set("locked = ?", lockValue).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Exec(ctx)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in updating dashboard", zap.String("uuid", uuid), zap.Error(err))
|
||||
return &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
@@ -241,17 +179,6 @@ func LockUnlockDashboard(ctx context.Context, uuid string, lock bool) *model.Api
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSlug updates the slug
|
||||
func (d *Dashboard) UpdateSlug() {
|
||||
var title string
|
||||
|
||||
if val, ok := d.Data["title"]; ok {
|
||||
title = val.(string)
|
||||
}
|
||||
|
||||
d.Slug = SlugifyTitle(title)
|
||||
}
|
||||
|
||||
func IsPostDataSane(data *map[string]interface{}) error {
|
||||
val, ok := (*data)["title"]
|
||||
if !ok || val == nil {
|
||||
@@ -261,21 +188,6 @@ func IsPostDataSane(data *map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SlugifyTitle(title string) string {
|
||||
s := slug.Make(strings.ToLower(title))
|
||||
if s == "" {
|
||||
// If the dashboard name is only characters outside of the
|
||||
// sluggable characters, the slug creation will return an
|
||||
// empty string which will mess up URLs. This failsafe picks
|
||||
// that up and creates the slug as a base64 identifier instead.
|
||||
s = base64.RawURLEncoding.EncodeToString([]byte(title))
|
||||
if slug.MaxLength != 0 && len(s) > slug.MaxLength {
|
||||
s = s[:slug.MaxLength]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func getWidgetIds(data map[string]interface{}) []string {
|
||||
widgetIds := []string{}
|
||||
if data != nil && data["widgets"] != nil {
|
||||
@@ -329,9 +241,8 @@ func getIdDifference(existingIds []string, newIds []string) []string {
|
||||
func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) {
|
||||
dashboardsInfo := model.DashboardsInfo{}
|
||||
// fetch dashboards from dashboard db
|
||||
query := "SELECT data FROM dashboards"
|
||||
var dashboardsData []Dashboard
|
||||
err := db.Select(&dashboardsData, query)
|
||||
dashboards := []types.Dashboard{}
|
||||
err := db.NewSelect().Model(&dashboards).Scan(ctx)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in processing sql query", zap.Error(err))
|
||||
return &dashboardsInfo, err
|
||||
@@ -340,7 +251,7 @@ func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) {
|
||||
var dashboardNames []string
|
||||
count := 0
|
||||
queriesWithTagAttrs := 0
|
||||
for _, dashboard := range dashboardsData {
|
||||
for _, dashboard := range dashboards {
|
||||
if isDashboardWithPanelAndName(dashboard.Data) {
|
||||
totalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + 1
|
||||
}
|
||||
@@ -371,7 +282,7 @@ func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) {
|
||||
}
|
||||
|
||||
dashboardsInfo.DashboardNames = dashboardNames
|
||||
dashboardsInfo.TotalDashboards = len(dashboardsData)
|
||||
dashboardsInfo.TotalDashboards = len(dashboards)
|
||||
dashboardsInfo.TotalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName
|
||||
dashboardsInfo.QueriesWithTSV2 = count
|
||||
dashboardsInfo.QueriesWithTagAttrs = queriesWithTagAttrs
|
||||
@@ -538,17 +449,13 @@ func countPanelsInDashboard(inputData map[string]interface{}) model.DashboardsIn
|
||||
}
|
||||
}
|
||||
|
||||
func GetDashboardsWithMetricNames(ctx context.Context, metricNames []string) (map[string][]map[string]string, *model.ApiError) {
|
||||
// Get all dashboards first
|
||||
query := `SELECT uuid, data FROM dashboards`
|
||||
|
||||
type dashboardRow struct {
|
||||
Uuid string `db:"uuid"`
|
||||
Data json.RawMessage `db:"data"`
|
||||
func GetDashboardsWithMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, *model.ApiError) {
|
||||
dashboards := []types.Dashboard{}
|
||||
err := db.NewSelect().Model(&dashboards).Where("org_id = ?", orgID).Scan(ctx)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in getting dashboards", zap.Error(err))
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
var dashboards []dashboardRow
|
||||
err := db.Select(&dashboards, query)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in getting dashboards", zap.Error(err))
|
||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
@@ -556,16 +463,10 @@ func GetDashboardsWithMetricNames(ctx context.Context, metricNames []string) (ma
|
||||
|
||||
// Initialize result map for each metric
|
||||
result := make(map[string][]map[string]string)
|
||||
// for _, metricName := range metricNames {
|
||||
// result[metricName] = []map[string]string{}
|
||||
// }
|
||||
|
||||
// Process the JSON data in Go
|
||||
for _, dashboard := range dashboards {
|
||||
var dashData map[string]interface{}
|
||||
if err := json.Unmarshal(dashboard.Data, &dashData); err != nil {
|
||||
continue
|
||||
}
|
||||
var dashData = dashboard.Data
|
||||
|
||||
dashTitle, _ := dashData["title"].(string)
|
||||
widgets, ok := dashData["widgets"].([]interface{})
|
||||
@@ -617,7 +518,7 @@ func GetDashboardsWithMetricNames(ctx context.Context, metricNames []string) (ma
|
||||
for _, metricName := range metricNames {
|
||||
if strings.TrimSpace(key) == metricName {
|
||||
result[metricName] = append(result[metricName], map[string]string{
|
||||
"dashboard_id": dashboard.Uuid,
|
||||
"dashboard_id": dashboard.UUID,
|
||||
"widget_title": widgetTitle,
|
||||
"widget_id": widgetID,
|
||||
"dashboard_title": dashTitle,
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
)
|
||||
|
||||
func readCurrentDir(dir string, fm interfaces.FeatureLookup) error {
|
||||
file, err := os.Open(dir)
|
||||
if err != nil {
|
||||
zap.L().Warn("failed opening directory", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
list, _ := file.Readdirnames(0) // 0 to read all files and folders
|
||||
for _, filename := range list {
|
||||
if strings.ToLower(filepath.Ext(filename)) != ".json" {
|
||||
zap.L().Debug("Skipping non-json file", zap.String("filename", filename))
|
||||
continue
|
||||
}
|
||||
zap.L().Info("Provisioning dashboard: ", zap.String("filename", filename))
|
||||
|
||||
// using filepath.Join for platform specific path creation
|
||||
// which is equivalent to "dir+/+filename" (on unix based systems) but cleaner
|
||||
plan, err := os.ReadFile(filepath.Join(dir, filename))
|
||||
if err != nil {
|
||||
zap.L().Error("Creating Dashboards: Error in reading json fron file", zap.String("filename", filename), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal(plan, &data)
|
||||
if err != nil {
|
||||
zap.L().Error("Creating Dashboards: Error in unmarshalling json from file", zap.String("filename", filename), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
err = IsPostDataSane(&data)
|
||||
if err != nil {
|
||||
zap.L().Info("Creating Dashboards: Error in file", zap.String("filename", filename), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
id := data["uuid"]
|
||||
if id == nil {
|
||||
_, apiErr := CreateDashboard(context.Background(), data, fm)
|
||||
if apiErr != nil {
|
||||
zap.L().Error("Creating Dashboards: Error in file", zap.String("filename", filename), zap.Error(apiErr.Err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
apiErr := upsertDashboard(id.(string), data, filename, fm)
|
||||
if apiErr != nil {
|
||||
zap.L().Error("Creating Dashboards: Error upserting dashboard", zap.String("filename", filename), zap.Error(apiErr.Err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertDashboard(uuid string, data map[string]interface{}, filename string, fm interfaces.FeatureLookup) *model.ApiError {
|
||||
_, apiErr := GetDashboard(context.Background(), uuid)
|
||||
if apiErr == nil {
|
||||
zap.S().Infof("Creating Dashboards: Already exists: %s\t%s", filename, "Dashboard already present in database, Updating dashboard")
|
||||
_, apiErr := UpdateDashboard(context.Background(), uuid, data, fm)
|
||||
return apiErr
|
||||
}
|
||||
|
||||
zap.S().Infof("Creating Dashboards: UUID not found: %s\t%s", filename, "Dashboard not present in database, Creating dashboard")
|
||||
_, apiErr = CreateDashboard(context.Background(), data, fm)
|
||||
return apiErr
|
||||
}
|
||||
|
||||
func LoadDashboardFiles(fm interfaces.FeatureLookup) error {
|
||||
dashboardsPath := constants.GetOrDefaultEnv("DASHBOARDS_PATH", "./config/dashboards")
|
||||
return readCurrentDir(dashboardsPath, fm)
|
||||
}
|
||||
@@ -9,45 +9,32 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/uptrace/bun"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
"go.signoz.io/signoz/pkg/types/authtypes"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var db *sqlx.DB
|
||||
|
||||
type SavedView struct {
|
||||
UUID string `json:"uuid" db:"uuid"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Category string `json:"category" db:"category"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
CreatedBy string `json:"created_by" db:"created_by"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
UpdatedBy string `json:"updated_by" db:"updated_by"`
|
||||
SourcePage string `json:"source_page" db:"source_page"`
|
||||
Tags string `json:"tags" db:"tags"`
|
||||
Data string `json:"data" db:"data"`
|
||||
ExtraData string `json:"extra_data" db:"extra_data"`
|
||||
}
|
||||
var db *bun.DB
|
||||
|
||||
// InitWithDSN sets up setting up the connection pool global variable.
|
||||
func InitWithDSN(inputDB *sqlx.DB) error {
|
||||
func InitWithDSN(inputDB *bun.DB) error {
|
||||
db = inputDB
|
||||
telemetry.GetInstance().SetSavedViewsInfoCallback(GetSavedViewsInfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitWithDB(sqlDB *sqlx.DB) {
|
||||
db = sqlDB
|
||||
func InitWithDB(bunDB *bun.DB) {
|
||||
db = bunDB
|
||||
}
|
||||
|
||||
func GetViews() ([]*v3.SavedView, error) {
|
||||
var views []SavedView
|
||||
err := db.Select(&views, "SELECT * FROM saved_views")
|
||||
func GetViews(ctx context.Context, orgID string) ([]*v3.SavedView, error) {
|
||||
var views []types.SavedView
|
||||
err := db.NewSelect().Model(&views).Where("org_id = ?", orgID).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in getting saved views: %s", err.Error())
|
||||
}
|
||||
@@ -76,13 +63,13 @@ func GetViews() ([]*v3.SavedView, error) {
|
||||
return savedViews, nil
|
||||
}
|
||||
|
||||
func GetViewsForFilters(sourcePage string, name string, category string) ([]*v3.SavedView, error) {
|
||||
var views []SavedView
|
||||
func GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*v3.SavedView, error) {
|
||||
var views []types.SavedView
|
||||
var err error
|
||||
if len(category) == 0 {
|
||||
err = db.Select(&views, "SELECT * FROM saved_views WHERE source_page = ? AND name LIKE ?", sourcePage, "%"+name+"%")
|
||||
err = db.NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND name LIKE ?", orgID, sourcePage, "%"+name+"%").Scan(ctx)
|
||||
} else {
|
||||
err = db.Select(&views, "SELECT * FROM saved_views WHERE source_page = ? AND category LIKE ? AND name LIKE ?", sourcePage, "%"+category+"%", "%"+name+"%")
|
||||
err = db.NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND category LIKE ? AND name LIKE ?", orgID, sourcePage, "%"+category+"%", "%"+name+"%").Scan(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in getting saved views: %s", err.Error())
|
||||
@@ -111,7 +98,7 @@ func GetViewsForFilters(sourcePage string, name string, category string) ([]*v3.
|
||||
return savedViews, nil
|
||||
}
|
||||
|
||||
func CreateView(ctx context.Context, view v3.SavedView) (string, error) {
|
||||
func CreateView(ctx context.Context, orgID string, view v3.SavedView) (string, error) {
|
||||
data, err := json.Marshal(view.CompositeQuery)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error in marshalling explorer query data: %s", err.Error())
|
||||
@@ -133,29 +120,35 @@ func CreateView(ctx context.Context, view v3.SavedView) (string, error) {
|
||||
createBy := claims.Email
|
||||
updatedBy := claims.Email
|
||||
|
||||
_, err = db.Exec(
|
||||
"INSERT INTO saved_views (uuid, name, category, created_at, created_by, updated_at, updated_by, source_page, tags, data, extra_data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
uuid_,
|
||||
view.Name,
|
||||
view.Category,
|
||||
createdAt,
|
||||
createBy,
|
||||
updatedAt,
|
||||
updatedBy,
|
||||
view.SourcePage,
|
||||
strings.Join(view.Tags, ","),
|
||||
data,
|
||||
view.ExtraData,
|
||||
)
|
||||
dbView := types.SavedView{
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: createBy,
|
||||
UpdatedBy: updatedBy,
|
||||
},
|
||||
OrgID: orgID,
|
||||
UUID: uuid_,
|
||||
Name: view.Name,
|
||||
Category: view.Category,
|
||||
SourcePage: view.SourcePage,
|
||||
Tags: strings.Join(view.Tags, ","),
|
||||
Data: string(data),
|
||||
ExtraData: view.ExtraData,
|
||||
}
|
||||
|
||||
_, err = db.NewInsert().Model(&dbView).Exec(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error in creating saved view: %s", err.Error())
|
||||
}
|
||||
return uuid_, nil
|
||||
}
|
||||
|
||||
func GetView(uuid_ string) (*v3.SavedView, error) {
|
||||
var view SavedView
|
||||
err := db.Get(&view, "SELECT * FROM saved_views WHERE uuid = ?", uuid_)
|
||||
func GetView(ctx context.Context, orgID string, uuid_ string) (*v3.SavedView, error) {
|
||||
var view types.SavedView
|
||||
err := db.NewSelect().Model(&view).Where("org_id = ? AND uuid = ?", orgID, uuid_).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in getting saved view: %s", err.Error())
|
||||
}
|
||||
@@ -180,7 +173,7 @@ func GetView(uuid_ string) (*v3.SavedView, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UpdateView(ctx context.Context, uuid_ string, view v3.SavedView) error {
|
||||
func UpdateView(ctx context.Context, orgID string, uuid_ string, view v3.SavedView) error {
|
||||
data, err := json.Marshal(view.CompositeQuery)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in marshalling explorer query data: %s", err.Error())
|
||||
@@ -194,16 +187,25 @@ func UpdateView(ctx context.Context, uuid_ string, view v3.SavedView) error {
|
||||
updatedAt := time.Now()
|
||||
updatedBy := claims.Email
|
||||
|
||||
_, err = db.Exec("UPDATE saved_views SET updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ? WHERE uuid = ?",
|
||||
updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData, uuid_)
|
||||
_, err = db.NewUpdate().
|
||||
Model(&types.SavedView{}).
|
||||
Set("updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ?",
|
||||
updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData).
|
||||
Where("uuid = ?", uuid_).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in updating saved view: %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteView(uuid_ string) error {
|
||||
_, err := db.Exec("DELETE FROM saved_views WHERE uuid = ?", uuid_)
|
||||
func DeleteView(ctx context.Context, orgID string, uuid_ string) error {
|
||||
_, err := db.NewDelete().
|
||||
Model(&types.SavedView{}).
|
||||
Where("uuid = ?", uuid_).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in deleting explorer query: %s", err.Error())
|
||||
}
|
||||
@@ -212,7 +214,17 @@ func DeleteView(uuid_ string) error {
|
||||
|
||||
func GetSavedViewsInfo(ctx context.Context) (*model.SavedViewsInfo, error) {
|
||||
savedViewsInfo := model.SavedViewsInfo{}
|
||||
savedViews, err := GetViews()
|
||||
// get single org ID from db
|
||||
var orgIDs []string
|
||||
err := db.NewSelect().Model((*types.Organization)(nil)).Column("id").Scan(ctx, &orgIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in getting org IDs: %s", err.Error())
|
||||
}
|
||||
if len(orgIDs) != 1 {
|
||||
zap.S().Warn("GetSavedViewsInfo: Zero or multiple org IDs found in the database", zap.Int("orgIDs", len(orgIDs)))
|
||||
return &savedViewsInfo, nil
|
||||
}
|
||||
savedViews, err := GetViews(ctx, orgIDs[0])
|
||||
if err != nil {
|
||||
zap.S().Debug("Error in fetching saved views info: ", err)
|
||||
return &savedViewsInfo, err
|
||||
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"time"
|
||||
|
||||
"go.signoz.io/signoz/pkg/alertmanager"
|
||||
errorsV2 "go.signoz.io/signoz/pkg/errors"
|
||||
"go.signoz.io/signoz/pkg/http/render"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/metricsexplorer"
|
||||
"go.signoz.io/signoz/pkg/signoz"
|
||||
|
||||
@@ -49,7 +51,6 @@ import (
|
||||
tracesV4 "go.signoz.io/signoz/pkg/query-service/app/traces/v4"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"go.signoz.io/signoz/pkg/query-service/contextlinks"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
@@ -274,11 +275,6 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
|
||||
}
|
||||
aH.queryBuilder = queryBuilder.NewQueryBuilder(builderOpts, aH.featureFlags)
|
||||
|
||||
dashboards.LoadDashboardFiles(aH.featureFlags)
|
||||
// if errReadingDashboards != nil {
|
||||
// return nil, errReadingDashboards
|
||||
// }
|
||||
|
||||
// check if at least one user is created
|
||||
hasUsers, err := aH.appDao.GetUsersWithOpts(context.Background(), 1)
|
||||
if err.Error() != "" {
|
||||
@@ -1059,7 +1055,12 @@ func (aH *APIHandler) listRules(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
allDashboards, err := dashboards.GetDashboards(r.Context())
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
allDashboards, err := dashboards.GetDashboards(r.Context(), claims.OrgID)
|
||||
if err != nil {
|
||||
RespondError(w, err, nil)
|
||||
return
|
||||
@@ -1113,7 +1114,7 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) {
|
||||
inter = Intersection(inter, tags2Dash[tag])
|
||||
}
|
||||
|
||||
filteredDashboards := []dashboards.Dashboard{}
|
||||
filteredDashboards := []types.Dashboard{}
|
||||
for _, val := range inter {
|
||||
dash := (allDashboards)[val]
|
||||
filteredDashboards = append(filteredDashboards, dash)
|
||||
@@ -1125,7 +1126,12 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) {
|
||||
func (aH *APIHandler) deleteDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
err := dashboards.DeleteDashboard(r.Context(), uuid, aH.featureFlags)
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
err := dashboards.DeleteDashboard(r.Context(), claims.OrgID, uuid, aH.featureFlags)
|
||||
|
||||
if err != nil {
|
||||
RespondError(w, err, nil)
|
||||
@@ -1212,7 +1218,12 @@ func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, apiError := dashboards.UpdateDashboard(r.Context(), uuid, postData, aH.featureFlags)
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
dashboard, apiError := dashboards.UpdateDashboard(r.Context(), claims.OrgID, claims.Email, uuid, postData, aH.featureFlags)
|
||||
if apiError != nil {
|
||||
RespondError(w, apiError, nil)
|
||||
return
|
||||
@@ -1226,7 +1237,12 @@ func (aH *APIHandler) getDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
|
||||
dashboard, apiError := dashboards.GetDashboard(r.Context(), uuid)
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
dashboard, apiError := dashboards.GetDashboard(r.Context(), claims.OrgID, uuid)
|
||||
|
||||
if apiError != nil {
|
||||
if apiError.Type() != model.ErrorNotFound {
|
||||
@@ -1275,8 +1291,12 @@ func (aH *APIHandler) createDashboards(w http.ResponseWriter, r *http.Request) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error reading request body")
|
||||
return
|
||||
}
|
||||
|
||||
dash, apiErr := dashboards.CreateDashboard(r.Context(), postData, aH.featureFlags)
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
dash, apiErr := dashboards.CreateDashboard(r.Context(), claims.OrgID, claims.Email, postData, aH.featureFlags)
|
||||
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -3385,10 +3405,14 @@ func (aH *APIHandler) getUserPreference(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
preferenceId := mux.Vars(r)["preferenceId"]
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
preference, apiErr := preferences.GetUserPreference(
|
||||
r.Context(), preferenceId, user.User.OrgID, user.User.ID,
|
||||
r.Context(), preferenceId, claims.OrgID, claims.UserID,
|
||||
)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -3402,7 +3426,11 @@ func (aH *APIHandler) updateUserPreference(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
preferenceId := mux.Vars(r)["preferenceId"]
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
req := preferences.UpdatePreference{}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
@@ -3411,7 +3439,7 @@ func (aH *APIHandler) updateUserPreference(
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.ID)
|
||||
preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, claims.UserID)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
@@ -3423,9 +3451,13 @@ func (aH *APIHandler) updateUserPreference(
|
||||
func (aH *APIHandler) getAllUserPreferences(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
preference, apiErr := preferences.GetAllUserPreferences(
|
||||
r.Context(), user.User.OrgID, user.User.ID,
|
||||
r.Context(), claims.OrgID, claims.UserID,
|
||||
)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -3439,9 +3471,13 @@ func (aH *APIHandler) getOrgPreference(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
preferenceId := mux.Vars(r)["preferenceId"]
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
preference, apiErr := preferences.GetOrgPreference(
|
||||
r.Context(), preferenceId, user.User.OrgID,
|
||||
r.Context(), preferenceId, claims.OrgID,
|
||||
)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -3456,7 +3492,11 @@ func (aH *APIHandler) updateOrgPreference(
|
||||
) {
|
||||
preferenceId := mux.Vars(r)["preferenceId"]
|
||||
req := preferences.UpdatePreference{}
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
@@ -3464,7 +3504,7 @@ func (aH *APIHandler) updateOrgPreference(
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.OrgID)
|
||||
preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, claims.OrgID)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
@@ -3476,9 +3516,13 @@ func (aH *APIHandler) updateOrgPreference(
|
||||
func (aH *APIHandler) getAllOrgPreferences(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
) {
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
preference, apiErr := preferences.GetAllOrgPreferences(
|
||||
r.Context(), user.User.OrgID,
|
||||
r.Context(), claims.OrgID,
|
||||
)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -4525,7 +4569,12 @@ func (aH *APIHandler) getSavedViews(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Query().Get("name")
|
||||
category := r.URL.Query().Get("category")
|
||||
|
||||
queries, err := explorer.GetViewsForFilters(sourcePage, name, category)
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
queries, err := explorer.GetViewsForFilters(r.Context(), claims.OrgID, sourcePage, name, category)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
@@ -4545,7 +4594,13 @@ func (aH *APIHandler) createSavedViews(w http.ResponseWriter, r *http.Request) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
uuid, err := explorer.CreateView(r.Context(), view)
|
||||
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
uuid, err := explorer.CreateView(r.Context(), claims.OrgID, view)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
@@ -4556,7 +4611,12 @@ func (aH *APIHandler) createSavedViews(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (aH *APIHandler) getSavedView(w http.ResponseWriter, r *http.Request) {
|
||||
viewID := mux.Vars(r)["viewId"]
|
||||
view, err := explorer.GetView(viewID)
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
view, err := explorer.GetView(r.Context(), claims.OrgID, viewID)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
@@ -4579,7 +4639,12 @@ func (aH *APIHandler) updateSavedView(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = explorer.UpdateView(r.Context(), viewID, view)
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
err = explorer.UpdateView(r.Context(), claims.OrgID, viewID, view)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
@@ -4591,7 +4656,12 @@ func (aH *APIHandler) updateSavedView(w http.ResponseWriter, r *http.Request) {
|
||||
func (aH *APIHandler) deleteSavedView(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
viewID := mux.Vars(r)["viewId"]
|
||||
err := explorer.DeleteView(viewID)
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
err := explorer.DeleteView(r.Context(), claims.OrgID, viewID)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/agentConf"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
@@ -130,12 +130,12 @@ func (c *Controller) GetPipelinesForInstalledIntegrations(
|
||||
|
||||
func (c *Controller) GetDashboardsForInstalledIntegrations(
|
||||
ctx context.Context,
|
||||
) ([]dashboards.Dashboard, *model.ApiError) {
|
||||
) ([]types.Dashboard, *model.ApiError) {
|
||||
return c.mgr.GetDashboardsForInstalledIntegrations(ctx)
|
||||
}
|
||||
|
||||
func (c *Controller) GetInstalledIntegrationDashboardById(
|
||||
ctx context.Context, dashboardUuid string,
|
||||
) (*dashboards.Dashboard, *model.ApiError) {
|
||||
) (*types.Dashboard, *model.ApiError) {
|
||||
return c.mgr.GetInstalledIntegrationDashboardById(ctx, dashboardUuid)
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/query-service/rules"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
)
|
||||
|
||||
type IntegrationAuthor struct {
|
||||
@@ -32,8 +32,8 @@ type IntegrationSummary struct {
|
||||
}
|
||||
|
||||
type IntegrationAssets struct {
|
||||
Logs LogsAssets `json:"logs"`
|
||||
Dashboards []dashboards.Data `json:"dashboards"`
|
||||
Logs LogsAssets `json:"logs"`
|
||||
Dashboards []types.DashboardData `json:"dashboards"`
|
||||
|
||||
Alerts []rules.PostableRule `json:"alerts"`
|
||||
}
|
||||
@@ -306,7 +306,7 @@ func (m *Manager) parseDashboardUuid(dashboardUuid string) (
|
||||
func (m *Manager) GetInstalledIntegrationDashboardById(
|
||||
ctx context.Context,
|
||||
dashboardUuid string,
|
||||
) (*dashboards.Dashboard, *model.ApiError) {
|
||||
) (*types.Dashboard, *model.ApiError) {
|
||||
integrationId, dashboardId, apiErr := m.parseDashboardUuid(dashboardUuid)
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
@@ -328,14 +328,18 @@ func (m *Manager) GetInstalledIntegrationDashboardById(
|
||||
if id, ok := dId.(string); ok && id == dashboardId {
|
||||
isLocked := 1
|
||||
author := "integration"
|
||||
return &dashboards.Dashboard{
|
||||
Uuid: m.dashboardUuid(integrationId, string(dashboardId)),
|
||||
Locked: &isLocked,
|
||||
Data: dd,
|
||||
CreatedAt: integration.Installation.InstalledAt,
|
||||
CreateBy: &author,
|
||||
UpdatedAt: integration.Installation.InstalledAt,
|
||||
UpdateBy: &author,
|
||||
return &types.Dashboard{
|
||||
UUID: m.dashboardUuid(integrationId, string(dashboardId)),
|
||||
Locked: &isLocked,
|
||||
Data: dd,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: integration.Installation.InstalledAt,
|
||||
UpdatedAt: integration.Installation.InstalledAt,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: author,
|
||||
UpdatedBy: author,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -348,13 +352,13 @@ func (m *Manager) GetInstalledIntegrationDashboardById(
|
||||
|
||||
func (m *Manager) GetDashboardsForInstalledIntegrations(
|
||||
ctx context.Context,
|
||||
) ([]dashboards.Dashboard, *model.ApiError) {
|
||||
) ([]types.Dashboard, *model.ApiError) {
|
||||
installedIntegrations, apiErr := m.getInstalledIntegrations(ctx)
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
result := []dashboards.Dashboard{}
|
||||
result := []types.Dashboard{}
|
||||
|
||||
for _, ii := range installedIntegrations {
|
||||
for _, dd := range ii.Assets.Dashboards {
|
||||
@@ -362,14 +366,18 @@ func (m *Manager) GetDashboardsForInstalledIntegrations(
|
||||
if dashboardId, ok := dId.(string); ok {
|
||||
isLocked := 1
|
||||
author := "integration"
|
||||
result = append(result, dashboards.Dashboard{
|
||||
Uuid: m.dashboardUuid(ii.IntegrationSummary.Id, dashboardId),
|
||||
Locked: &isLocked,
|
||||
Data: dd,
|
||||
CreatedAt: ii.Installation.InstalledAt,
|
||||
CreateBy: &author,
|
||||
UpdatedAt: ii.Installation.InstalledAt,
|
||||
UpdateBy: &author,
|
||||
result = append(result, types.Dashboard{
|
||||
UUID: m.dashboardUuid(ii.IntegrationSummary.Id, dashboardId),
|
||||
Locked: &isLocked,
|
||||
Data: dd,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: ii.Installation.InstalledAt,
|
||||
UpdatedAt: ii.Installation.InstalledAt,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: author,
|
||||
UpdatedBy: author,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/rules"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
)
|
||||
|
||||
func NewTestIntegrationsManager(t *testing.T) *Manager {
|
||||
@@ -92,7 +92,7 @@ func (t *TestAvailableIntegrationsRepo) list(
|
||||
},
|
||||
},
|
||||
},
|
||||
Dashboards: []dashboards.Data{},
|
||||
Dashboards: []types.DashboardData{},
|
||||
Alerts: []rules.PostableRule{},
|
||||
},
|
||||
ConnectionTests: &IntegrationConnectionTests{
|
||||
@@ -160,7 +160,7 @@ func (t *TestAvailableIntegrationsRepo) list(
|
||||
},
|
||||
},
|
||||
},
|
||||
Dashboards: []dashboards.Data{},
|
||||
Dashboards: []types.DashboardData{},
|
||||
Alerts: []rules.PostableRule{},
|
||||
},
|
||||
ConnectionTests: &IntegrationConnectionTests{
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/model/metrics_explorer"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/rules"
|
||||
"go.signoz.io/signoz/pkg/types/authtypes"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
@@ -155,7 +156,11 @@ func (receiver *SummaryService) GetMetricsSummary(ctx context.Context, metricNam
|
||||
g.Go(func() error {
|
||||
var metricNames []string
|
||||
metricNames = append(metricNames, metricName)
|
||||
data, err := dashboards.GetDashboardsWithMetricNames(ctx, metricNames)
|
||||
claims, ok := authtypes.ClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return &model.ApiError{Typ: model.ErrorInternal, Err: errors.New("failed to get claims")}
|
||||
}
|
||||
data, err := dashboards.GetDashboardsWithMetricNames(ctx, claims.OrgID, metricNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -322,7 +327,11 @@ func (receiver *SummaryService) GetRelatedMetrics(ctx context.Context, params *m
|
||||
alertsRelatedData := make(map[string][]metrics_explorer.Alert)
|
||||
|
||||
g.Go(func() error {
|
||||
names, apiError := dashboards.GetDashboardsWithMetricNames(ctx, metricNames)
|
||||
claims, ok := authtypes.ClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return &model.ApiError{Typ: model.ErrorInternal, Err: errors.New("failed to get claims")}
|
||||
}
|
||||
names, apiError := dashboards.GetDashboardsWithMetricNames(ctx, claims.OrgID, metricNames)
|
||||
if apiError != nil {
|
||||
return apiError
|
||||
}
|
||||
|
||||
@@ -101,11 +101,11 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil {
|
||||
if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore.BunDB()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := explorer.InitWithDSN(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil {
|
||||
if err := explorer.InitWithDSN(serverOptions.SigNoz.SQLStore.BunDB()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ func IsViewer(user *types.GettableUser) bool { return user.GroupID == AuthCacheO
|
||||
func IsEditor(user *types.GettableUser) bool { return user.GroupID == AuthCacheObj.EditorGroupId }
|
||||
func IsAdmin(user *types.GettableUser) bool { return user.GroupID == AuthCacheObj.AdminGroupId }
|
||||
|
||||
func IsAdminV2(claims authtypes.Claims) bool { return claims.GroupID == AuthCacheObj.AdminGroupId }
|
||||
|
||||
func ValidatePassword(password string) error {
|
||||
if len(password) < minimumPasswordLength {
|
||||
return errors.Errorf("Password should be atleast %d characters.", minimumPasswordLength)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
)
|
||||
|
||||
func GetUserFromContext(ctx context.Context) *types.GettableUser {
|
||||
user, ok := ctx.Value(constants.ContextUserKey).(*types.GettableUser)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func PrepareLinksToLogs(start, end time.Time, filterItems []v3.FilterItem) strin
|
||||
// i.e Severity text = WARN
|
||||
// If the Severity text is not part of the group by clause, then we add it as it is
|
||||
func PrepareFilters(labels map[string]string, whereClauseItems []v3.FilterItem, groupByItems []v3.AttributeKey, keys map[string]v3.AttributeKey) []v3.FilterItem {
|
||||
var filterItems []v3.FilterItem
|
||||
filterItems := make([]v3.FilterItem, 0)
|
||||
|
||||
added := make(map[string]struct{})
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"go.signoz.io/signoz/pkg/http/middleware"
|
||||
"go.signoz.io/signoz/pkg/query-service/app"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/integrations"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
@@ -350,7 +349,7 @@ func TestDashboardsForInstalledIntegrationDashboards(t *testing.T) {
|
||||
require.GreaterOrEqual(dashboards[0].UpdatedAt.Unix(), tsBeforeInstallation)
|
||||
|
||||
// Should be able to get installed integrations dashboard by id
|
||||
dd := integrationsTB.GetDashboardByIdFromQS(dashboards[0].Uuid)
|
||||
dd := integrationsTB.GetDashboardByIdFromQS(dashboards[0].UUID)
|
||||
require.GreaterOrEqual(dd.CreatedAt.Unix(), tsBeforeInstallation)
|
||||
require.GreaterOrEqual(dd.UpdatedAt.Unix(), tsBeforeInstallation)
|
||||
require.Equal(*dd, dashboards[0])
|
||||
@@ -464,7 +463,7 @@ func (tb *IntegrationsTestBed) RequestQSToUninstallIntegration(
|
||||
tb.RequestQS("/api/v1/integrations/uninstall", request)
|
||||
}
|
||||
|
||||
func (tb *IntegrationsTestBed) GetDashboardsFromQS() []dashboards.Dashboard {
|
||||
func (tb *IntegrationsTestBed) GetDashboardsFromQS() []types.Dashboard {
|
||||
result := tb.RequestQS("/api/v1/dashboards", nil)
|
||||
|
||||
dataJson, err := json.Marshal(result.Data)
|
||||
@@ -472,7 +471,7 @@ func (tb *IntegrationsTestBed) GetDashboardsFromQS() []dashboards.Dashboard {
|
||||
tb.t.Fatalf("could not marshal apiResponse.Data: %v", err)
|
||||
}
|
||||
|
||||
dashboards := []dashboards.Dashboard{}
|
||||
dashboards := []types.Dashboard{}
|
||||
err = json.Unmarshal(dataJson, &dashboards)
|
||||
if err != nil {
|
||||
tb.t.Fatalf(" could not unmarshal apiResponse.Data json into dashboards")
|
||||
@@ -481,7 +480,7 @@ func (tb *IntegrationsTestBed) GetDashboardsFromQS() []dashboards.Dashboard {
|
||||
return dashboards
|
||||
}
|
||||
|
||||
func (tb *IntegrationsTestBed) GetDashboardByIdFromQS(dashboardUuid string) *dashboards.Dashboard {
|
||||
func (tb *IntegrationsTestBed) GetDashboardByIdFromQS(dashboardUuid string) *types.Dashboard {
|
||||
result := tb.RequestQS(fmt.Sprintf("/api/v1/dashboards/%s", dashboardUuid), nil)
|
||||
|
||||
dataJson, err := json.Marshal(result.Data)
|
||||
@@ -489,7 +488,7 @@ func (tb *IntegrationsTestBed) GetDashboardByIdFromQS(dashboardUuid string) *das
|
||||
tb.t.Fatalf("could not marshal apiResponse.Data: %v", err)
|
||||
}
|
||||
|
||||
dashboard := dashboards.Dashboard{}
|
||||
dashboard := types.Dashboard{}
|
||||
err = json.Unmarshal(dataJson, &dashboard)
|
||||
if err != nil {
|
||||
tb.t.Fatalf(" could not unmarshal apiResponse.Data json into dashboards")
|
||||
|
||||
@@ -48,6 +48,7 @@ func NewTestSqliteDB(t *testing.T) (sqlStore sqlstore.SQLStore, testDBFilePath s
|
||||
sqlmigration.NewModifyDatetimeFactory(),
|
||||
sqlmigration.NewModifyOrgDomainFactory(),
|
||||
sqlmigration.NewUpdateOrganizationFactory(sqlStore),
|
||||
sqlmigration.NewUpdateDashboardAndSavedViewsFactory(sqlStore),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -69,7 +70,7 @@ func NewQueryServiceDBForTests(t *testing.T) sqlstore.SQLStore {
|
||||
if err != nil {
|
||||
t.Fatalf("could not initialize dao: %v", err)
|
||||
}
|
||||
dashboards.InitDB(sqlStore.SQLxDB())
|
||||
dashboards.InitDB(sqlStore.BunDB())
|
||||
|
||||
return sqlStore
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
|
||||
sqlmigration.NewModifyOrgDomainFactory(),
|
||||
sqlmigration.NewUpdateOrganizationFactory(sqlstore),
|
||||
sqlmigration.NewAddAlertmanagerFactory(),
|
||||
sqlmigration.NewUpdateDashboardAndSavedViewsFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
80
pkg/sqlmigration/015_update_dashboards_savedviews.go
Normal file
80
pkg/sqlmigration/015_update_dashboards_savedviews.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.signoz.io/signoz/pkg/types"
|
||||
)
|
||||
|
||||
type updateDashboardAndSavedViews struct {
|
||||
store sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewUpdateDashboardAndSavedViewsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("update_group"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return newUpdateDashboardAndSavedViews(ctx, ps, c, sqlstore)
|
||||
})
|
||||
}
|
||||
|
||||
func newUpdateDashboardAndSavedViews(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) {
|
||||
return &updateDashboardAndSavedViews{
|
||||
store: store,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (migration *updateDashboardAndSavedViews) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *updateDashboardAndSavedViews) Up(ctx context.Context, db *bun.DB) error {
|
||||
|
||||
// begin transaction
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// get all org ids
|
||||
var orgIDs []string
|
||||
if err := migration.store.BunDB().NewSelect().Model((*types.Organization)(nil)).Column("id").Scan(ctx, &orgIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add org id to dashboards table
|
||||
for _, table := range []string{"dashboards", "saved_views"} {
|
||||
if exists, err := migration.store.Dialect().ColumnExists(ctx, tx, table, "org_id"); err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
if _, err := tx.NewAddColumn().Table(table).ColumnExpr("org_id TEXT REFERENCES organizations(id) ON DELETE CASCADE").Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if there is one org ID if yes then set it to all dashboards.
|
||||
if len(orgIDs) == 1 {
|
||||
orgID := orgIDs[0]
|
||||
if _, err := tx.NewUpdate().Table(table).Set("org_id = ?", orgID).Where("org_id IS NULL").Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *updateDashboardAndSavedViews) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
28
pkg/types/alertmanagertypes/template.go
Normal file
28
pkg/types/alertmanagertypes/template.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package alertmanagertypes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
alertmanagertemplate "github.com/prometheus/alertmanager/template"
|
||||
)
|
||||
|
||||
// FromGlobs overrides the default alertmanager template to add a ruleIdPath template.
|
||||
// This is used to generate a link to the rule in the alertmanager.
|
||||
//
|
||||
// It explicitly checks for a ruleId that is a number and then generates a path to the rule.
|
||||
func FromGlobs(paths []string) (*alertmanagertemplate.Template, error) {
|
||||
t, err := alertmanagertemplate.FromGlobs(paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := t.Parse(bytes.NewReader([]byte(`
|
||||
{{ define "__ruleIdPath" }}{{ range .CommonLabels.SortedPairs }}{{ if eq .Name "ruleId" }}{{ if match "^[0-9]+$" .Value }}/edit?ruleId={{ .Value | urlquery }}{{ end }}{{ end }}{{ end }}{{ end }}
|
||||
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}/alerts{{ template "__ruleIdPath" . }}{{ end }}
|
||||
`))); err != nil {
|
||||
return nil, fmt.Errorf("error parsing alertmanager templates: %w", err)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
141
pkg/types/alertmanagertypes/template_test.go
Normal file
141
pkg/types/alertmanagertypes/template_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package alertmanagertypes
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFromGlobs(t *testing.T) {
|
||||
template, err := FromGlobs([]string{})
|
||||
require.NoError(t, err)
|
||||
template.ExternalURL = &url.URL{Scheme: "http", Host: "localhost:8080", Path: ""}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
alerts []*types.Alert
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "SingleAlertWithValidRuleId",
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"ruleId": "439453587",
|
||||
},
|
||||
},
|
||||
UpdatedAt: time.Now(),
|
||||
Timeout: false,
|
||||
},
|
||||
},
|
||||
expected: "http://localhost:8080/alerts/edit?ruleId=439453587",
|
||||
},
|
||||
{
|
||||
name: "SingleAlertWithInvalidRuleId",
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"ruleId": "43textabc",
|
||||
},
|
||||
},
|
||||
UpdatedAt: time.Now(),
|
||||
Timeout: false,
|
||||
},
|
||||
},
|
||||
expected: "http://localhost:8080/alerts",
|
||||
},
|
||||
{
|
||||
name: "MultipleAlertsWithMismatchingRuleId",
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"ruleId": "1",
|
||||
},
|
||||
},
|
||||
UpdatedAt: time.Now(),
|
||||
Timeout: false,
|
||||
},
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"ruleId": "2",
|
||||
},
|
||||
},
|
||||
UpdatedAt: time.Now(),
|
||||
Timeout: false,
|
||||
},
|
||||
},
|
||||
expected: "http://localhost:8080/alerts",
|
||||
},
|
||||
{
|
||||
name: "MultipleAlertsWithMatchingRuleId",
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"ruleId": "1",
|
||||
},
|
||||
},
|
||||
UpdatedAt: time.Now(),
|
||||
Timeout: false,
|
||||
},
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"ruleId": "1",
|
||||
},
|
||||
},
|
||||
UpdatedAt: time.Now(),
|
||||
Timeout: false,
|
||||
},
|
||||
},
|
||||
expected: "http://localhost:8080/alerts/edit?ruleId=1",
|
||||
},
|
||||
{
|
||||
name: "MultipleAlertsWithNoRuleId",
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"label1": "1",
|
||||
},
|
||||
},
|
||||
UpdatedAt: time.Now(),
|
||||
Timeout: false,
|
||||
},
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"label2": "2",
|
||||
},
|
||||
},
|
||||
UpdatedAt: time.Now(),
|
||||
Timeout: false,
|
||||
},
|
||||
},
|
||||
expected: "http://localhost:8080/alerts",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data := template.Data("__receiver", model.LabelSet{}, tc.alerts...)
|
||||
|
||||
url, err := template.ExecuteTextString(`{{ template "__alertmanagerURL" . }}`, data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, url)
|
||||
|
||||
url, err = template.ExecuteHTMLString(`{{ template "__alertmanagerURL" . }}`, data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, url)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,71 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type Dashboard struct {
|
||||
bun.BaseModel `bun:"table:dashboards"`
|
||||
|
||||
ID int `bun:"id,pk,autoincrement"`
|
||||
UUID string `bun:"uuid,type:text,notnull,unique"`
|
||||
CreatedAt time.Time `bun:"created_at,type:datetime,notnull"`
|
||||
CreatedBy string `bun:"created_by,type:text,notnull"`
|
||||
UpdatedAt time.Time `bun:"updated_at,type:datetime,notnull"`
|
||||
UpdatedBy string `bun:"updated_by,type:text,notnull"`
|
||||
Data string `bun:"data,type:text,notnull"`
|
||||
Locked int `bun:"locked,notnull,default:0"`
|
||||
TimeAuditable
|
||||
UserAuditable
|
||||
OrgID string `json:"-" bun:"org_id,notnull"`
|
||||
ID int `json:"id" bun:"id,pk,autoincrement"`
|
||||
UUID string `json:"uuid" bun:"uuid,type:text,notnull,unique"`
|
||||
Data DashboardData `json:"data" bun:"data,type:text,notnull"`
|
||||
Locked *int `json:"isLocked" bun:"locked,notnull,default:0"`
|
||||
|
||||
Slug string `json:"-" bun:"-"`
|
||||
Title string `json:"-" bun:"-"`
|
||||
}
|
||||
|
||||
// UpdateSlug updates the slug
|
||||
func (d *Dashboard) UpdateSlug() {
|
||||
var title string
|
||||
|
||||
if val, ok := d.Data["title"]; ok {
|
||||
title = val.(string)
|
||||
}
|
||||
|
||||
d.Slug = SlugifyTitle(title)
|
||||
}
|
||||
|
||||
func SlugifyTitle(title string) string {
|
||||
s := slug.Make(strings.ToLower(title))
|
||||
if s == "" {
|
||||
// If the dashboard name is only characters outside of the
|
||||
// sluggable characters, the slug creation will return an
|
||||
// empty string which will mess up URLs. This failsafe picks
|
||||
// that up and creates the slug as a base64 identifier instead.
|
||||
s = base64.RawURLEncoding.EncodeToString([]byte(title))
|
||||
if slug.MaxLength != 0 && len(s) > slug.MaxLength {
|
||||
s = s[:slug.MaxLength]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type DashboardData map[string]interface{}
|
||||
|
||||
func (c DashboardData) Value() (driver.Value, error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
func (c *DashboardData) Scan(src interface{}) error {
|
||||
var data []byte
|
||||
if b, ok := src.([]byte); ok {
|
||||
data = b
|
||||
} else if s, ok := src.(string); ok {
|
||||
data = []byte(s)
|
||||
}
|
||||
return json.Unmarshal(data, c)
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type SavedView struct {
|
||||
bun.BaseModel `bun:"table:saved_views"`
|
||||
|
||||
UUID string `bun:"uuid,pk,type:text"`
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
Category string `bun:"category,type:text,notnull"`
|
||||
CreatedAt time.Time `bun:"created_at,type:datetime,notnull"`
|
||||
CreatedBy string `bun:"created_by,type:text"`
|
||||
UpdatedAt time.Time `bun:"updated_at,type:datetime,notnull"`
|
||||
UpdatedBy string `bun:"updated_by,type:text"`
|
||||
SourcePage string `bun:"source_page,type:text,notnull"`
|
||||
Tags string `bun:"tags,type:text"`
|
||||
Data string `bun:"data,type:text,notnull"`
|
||||
ExtraData string `bun:"extra_data,type:text"`
|
||||
TimeAuditable
|
||||
UserAuditable
|
||||
OrgID string `json:"orgId" bun:"org_id,notnull"`
|
||||
UUID string `json:"uuid" bun:"uuid,pk,type:text"`
|
||||
Name string `json:"name" bun:"name,type:text,notnull"`
|
||||
Category string `json:"category" bun:"category,type:text,notnull"`
|
||||
SourcePage string `json:"sourcePage" bun:"source_page,type:text,notnull"`
|
||||
Tags string `json:"tags" bun:"tags,type:text"`
|
||||
Data string `json:"data" bun:"data,type:text,notnull"`
|
||||
ExtraData string `json:"extraData" bun:"extra_data,type:text"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user