Compare commits

..

1 Commits

Author SHA1 Message Date
Ishan Uniyal
5320138eb9 fix: added event propogation on enter press 2026-03-13 13:01:21 +05:30
45 changed files with 587 additions and 2565 deletions

5
.github/CODEOWNERS vendored
View File

@@ -127,15 +127,12 @@
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend /frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend /frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
# Dashboard Widget Page
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Dashboard Page ## Dashboard Page
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend /frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend /frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend /frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Public Dashboard Page ## Public Dashboard Page

View File

@@ -13,11 +13,8 @@ import (
"github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway" "github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/gateway/noopgateway" "github.com/SigNoz/signoz/pkg/gateway/noopgateway"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing" "github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/modules/organization"
@@ -91,9 +88,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler { func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
return querier.NewHandler(ps, q, a) return querier.NewHandler(ps, q, a)
}, },
func(_ sqlstore.SQLStore, _ licensing.Licensing, _ zeus.Zeus, _ gateway.Gateway, _ global.Config) cloudintegration.Module {
return implcloudintegration.NewModule()
},
) )
if err != nil { if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", "error", err) logger.ErrorContext(ctx, "failed to create signoz", "error", err)

View File

@@ -9,13 +9,12 @@ import (
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn" "github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn" "github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
"github.com/SigNoz/signoz/ee/authz/openfgaauthz" "github.com/SigNoz/signoz/ee/authz/openfgaauthz"
eequerier "github.com/SigNoz/signoz/ee/querier"
"github.com/SigNoz/signoz/ee/authz/openfgaschema" "github.com/SigNoz/signoz/ee/authz/openfgaschema"
"github.com/SigNoz/signoz/ee/gateway/httpgateway" "github.com/SigNoz/signoz/ee/gateway/httpgateway"
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing" enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing" "github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard" "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
eequerier "github.com/SigNoz/signoz/ee/querier"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app" enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema" "github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore" "github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
@@ -26,10 +25,7 @@ import (
"github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway" "github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
pkgimplcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard" pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/modules/organization"
@@ -133,9 +129,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
communityHandler := querier.NewHandler(ps, q, a) communityHandler := querier.NewHandler(ps, q, a)
return eequerier.NewHandler(ps, q, communityHandler) return eequerier.NewHandler(ps, q, communityHandler)
}, },
func(store sqlstore.SQLStore, lic licensing.Licensing, z zeus.Zeus, gw gateway.Gateway, gc global.Config) cloudintegration.Module {
return implcloudintegration.NewModule(pkgimplcloudintegration.NewStore(store), store, lic, z, gw, gc)
},
) )
if err != nil { if err != nil {

View File

@@ -1,22 +0,0 @@
package implcloudintegration
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type awsProvider struct{}
func (p *awsProvider) CreateArtifact(
_ context.Context,
_ valuer.UUID,
_ *cloudintegrationtypes.ConnectionArtifactRequest,
_ cloudintegration.Credentials,
_ valuer.UUID,
) (*cloudintegrationtypes.ConnectionArtifact, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}

View File

@@ -1,23 +0,0 @@
package implcloudintegration
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// keeping this for example to show how more cloud providers will be added
type azureProvider struct{}
func (p *azureProvider) CreateArtifact(
_ context.Context,
_ valuer.UUID,
_ *cloudintegrationtypes.ConnectionArtifactRequest,
_ cloudintegration.Credentials,
_ valuer.UUID,
) (*cloudintegrationtypes.ConnectionArtifact, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}

View File

@@ -1,267 +0,0 @@
package implcloudintegration
import (
"context"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
)
type module struct {
store cloudintegrationtypes.Store
userStore types.UserStore
licensing licensing.Licensing
zeus zeus.Zeus
gateway gateway.Gateway
globalConfig global.Config
providers map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProvider
}
func NewModule(
store cloudintegrationtypes.Store,
sqlStore sqlstore.SQLStore,
lic licensing.Licensing,
z zeus.Zeus,
gw gateway.Gateway,
gc global.Config,
) cloudintegration.Module {
return &module{
store: store,
userStore: impluser.NewStore(sqlStore, factory.ProviderSettings{}),
licensing: lic,
zeus: z,
gateway: gw,
globalConfig: gc,
providers: map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProvider{
cloudintegrationtypes.CloudProviderTypeAWS: &awsProvider{},
cloudintegrationtypes.CloudProviderTypeAzure: &azureProvider{},
},
}
}
func (m *module) CreateConnectionArtifact(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, request *cloudintegrationtypes.ConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
p, ok := m.providers[provider]
if !ok {
return nil, errors.NewInvalidInputf(cloudintegrationtypes.ErrCodeCloudProviderInvalidInput, "unsupported cloud provider: %s", provider.StringValue())
}
creds, err := m.resolveCredentials(ctx, orgID, provider)
if err != nil {
return nil, err
}
newAccountID := valuer.GenerateUUID()
artifact, err := p.CreateArtifact(ctx, orgID, request, creds, newAccountID)
if err != nil {
return nil, err
}
account := &cloudintegrationtypes.StorableCloudIntegration{
Identifiable: types.Identifiable{ID: newAccountID},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Provider: provider,
OrgID: orgID,
}
if err := m.store.UpsertAccount(ctx, account); err != nil {
return nil, err
}
return artifact, nil
}
func (m *module) resolveCredentials(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (cloudintegration.Credentials, error) {
creds := cloudintegration.Credentials{}
pat, err := m.getOrCreateIntegrationPAT(ctx, orgID, provider)
if err != nil {
return creds, err
}
creds.SigNozAPIKey = pat
if m.licensing == nil {
return creds, nil
}
license, err := m.licensing.GetActive(ctx, orgID)
if err != nil {
return creds, err
}
if license == nil {
return creds, nil
}
respBytes, err := m.zeus.GetDeployment(ctx, license.Key)
if err != nil {
return creds, errors.NewInternalf(errors.CodeInternal, "couldn't query deployment info: %v", err)
}
deployment, err := zeustypes.NewGettableDeployment(respBytes)
if err != nil {
return creds, err
}
creds.SigNozAPIUrl = deployment.SignozAPIUrl
if m.globalConfig.IngestionURL != nil {
creds.IngestionUrl = m.globalConfig.IngestionURL.String()
}
if m.gateway != nil {
ingestionKeyName := fmt.Sprintf("%s-integration", provider.StringValue())
ingestionKey, err := m.getOrCreateIngestionKey(ctx, orgID, ingestionKeyName)
if err != nil {
return creds, err
}
creds.IngestionKey = ingestionKey
}
return creds, nil
}
func (m *module) getOrCreateIngestionKey(ctx context.Context, orgID valuer.UUID, keyName string) (string, error) {
result, err := m.gateway.SearchIngestionKeysByName(ctx, orgID, keyName, 1, 10)
if err != nil {
return "", errors.NewInternalf(errors.CodeInternal, "couldn't search ingestion keys: %v", err)
}
for _, k := range result.Keys {
if k.Name == keyName {
return k.Value, nil
}
}
created, err := m.gateway.CreateIngestionKey(ctx, orgID, keyName, []string{"integration"}, time.Time{})
if err != nil {
return "", errors.NewInternalf(errors.CodeInternal, "couldn't create ingestion key: %v", err)
}
return created.Value, nil
}
func (m *module) getOrCreateIntegrationPAT(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (string, error) {
integrationPATName := fmt.Sprintf("%s integration", provider.StringValue())
integrationUser, err := m.getOrCreateIntegrationUser(ctx, orgID, provider)
if err != nil {
return "", err
}
allPATs, err := m.userStore.ListAPIKeys(ctx, orgID)
if err != nil {
return "", errors.NewInternalf(errors.CodeInternal, "couldn't list PATs: %v", err)
}
for _, p := range allPATs {
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
return p.Token, nil
}
}
newPAT, err := types.NewStorableAPIKey(
integrationPATName,
integrationUser.ID,
types.RoleViewer,
0,
)
if err != nil {
return "", errors.NewInternalf(errors.CodeInternal, "couldn't create cloud integration PAT: %v", err)
}
if err := m.userStore.CreateAPIKey(ctx, newPAT); err != nil {
return "", errors.NewInternalf(errors.CodeInternal, "couldn't persist cloud integration PAT: %v", err)
}
return newPAT.Token, nil
}
func (m *module) getOrCreateIntegrationUser(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*types.User, error) {
cloudIntegrationUserName := fmt.Sprintf("%s-integration", provider.StringValue())
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
existingUsers, err := m.userStore.GetUsersByEmailAndOrgID(ctx, email, orgID)
if err != nil {
return nil, errors.NewInternalf(errors.CodeInternal, "couldn't look up integration user: %v", err)
}
for _, u := range existingUsers {
if u.Status != types.UserStatusDeleted {
return u, nil
}
}
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, orgID, types.UserStatusActive)
if err != nil {
return nil, errors.NewInternalf(errors.CodeInternal, "couldn't construct integration user: %v", err)
}
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
if err := m.userStore.CreateUser(ctx, cloudIntegrationUser); err != nil {
return nil, errors.NewInternalf(errors.CodeInternal, "couldn't create integration user: %v", err)
}
if err := m.userStore.CreatePassword(ctx, password); err != nil {
return nil, errors.NewInternalf(errors.CodeInternal, "couldn't create integration user password: %v", err)
}
return cloudIntegrationUser, nil
}
func (m *module) GetAccountStatus(_ context.Context, _, _ valuer.UUID) (*cloudintegrationtypes.AccountStatus, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}
func (m *module) ListConnectedAccounts(_ context.Context, _ valuer.UUID) (*cloudintegrationtypes.ConnectedAccounts, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}
func (m *module) DisconnectAccount(_ context.Context, _, _ valuer.UUID) error {
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}
func (m *module) UpdateAccountConfig(_ context.Context, _, _ valuer.UUID, _ *cloudintegrationtypes.UpdateAccountConfigRequest) (*cloudintegrationtypes.Account, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}
func (m *module) ListServicesSummary(_ context.Context, _ valuer.UUID, _ *valuer.UUID) (*cloudintegrationtypes.ServicesSummary, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}
func (m *module) GetService(_ context.Context, _ valuer.UUID, _ string, _ *valuer.UUID) (*cloudintegrationtypes.Service, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}
func (m *module) UpdateServiceConfig(_ context.Context, _ string, _ valuer.UUID, _ *cloudintegrationtypes.UpdateServiceConfigRequest) (*cloudintegrationtypes.ServiceSummary, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}
func (m *module) AgentCheckIn(_ context.Context, _ valuer.UUID, _ *cloudintegrationtypes.AgentCheckInRequest) (cloudintegrationtypes.AgentCheckInResponse, error) {
return cloudintegrationtypes.AgentCheckInResponse{}, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}
func (m *module) GetDashboardByID(_ context.Context, _ string, _ valuer.UUID) (*dashboardtypes.Dashboard, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}
func (m *module) GetAllDashboards(_ context.Context, _ valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
}

View File

@@ -302,6 +302,7 @@ function CustomTimePicker({
): void => { ): void => {
event?.preventDefault(); event?.preventDefault();
event?.stopPropagation(); event?.stopPropagation();
// check if the entered time is in the format of 1m, 2h, 3d, 4w // check if the entered time is in the format of 1m, 2h, 3d, 4w
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue); const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);

View File

@@ -9,6 +9,7 @@ import {
} from 'hooks/dashboard/useDashboardVariables'; } from 'hooks/dashboard/useDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl'; import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore'; import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import { import {
enqueueDescendantsOfVariable, enqueueDescendantsOfVariable,
@@ -29,7 +30,7 @@ function DashboardVariableSelection(): JSX.Element | null {
updateLocalStorageDashboardVariables, updateLocalStorageDashboardVariables,
} = useDashboard(); } = useDashboard();
const { updateUrlVariable } = useVariablesFromUrl(); const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables(); const { dashboardVariables } = useDashboardVariables();
const dashboardId = useDashboardVariablesSelector( const dashboardId = useDashboardVariablesSelector(
@@ -49,6 +50,15 @@ function DashboardVariableSelection(): JSX.Element | null {
(state) => state.globalTime, (state) => state.globalTime,
); );
useEffect(() => {
// Initialize variables with default values if not in URL
initializeDefaultVariables(
dashboardVariables,
getUrlVariables,
updateUrlVariable,
);
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
// Memoize the order key to avoid unnecessary triggers // Memoize the order key to avoid unnecessary triggers
const variableOrderKey = useMemo(() => { const variableOrderKey = useMemo(() => {
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? ''; const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';

View File

@@ -1,7 +1,5 @@
.column-unit-selector { .column-unit-selector {
display: flex; margin-top: 16px;
flex-direction: column;
gap: 8px;
.heading { .heading {
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
@@ -32,11 +30,6 @@
width: 100%; width: 100%;
} }
} }
&-content {
display: flex;
flex-direction: column;
gap: 12px;
}
} }
.lightMode { .lightMode {

View File

@@ -72,24 +72,22 @@ export function ColumnUnitSelector(
return ( return (
<section className="column-unit-selector"> <section className="column-unit-selector">
<Typography.Text className="heading">Column Units</Typography.Text> <Typography.Text className="heading">Column Units</Typography.Text>
<div className="column-unit-selector-content"> {aggregationQueries.map(({ value, label }) => {
{aggregationQueries.map(({ value, label }) => { const baseQueryName = value.split('.')[0];
const baseQueryName = value.split('.')[0]; return (
return ( <YAxisUnitSelectorV2
<YAxisUnitSelectorV2 value={columnUnits[value] || ''}
value={columnUnits[value] || ''} onSelect={(unitValue: string): void =>
onSelect={(unitValue: string): void => handleColumnUnitSelect(value, unitValue)
handleColumnUnitSelect(value, unitValue) }
} fieldLabel={label}
fieldLabel={label} key={value}
key={value} selectedQueryName={baseQueryName}
selectedQueryName={baseQueryName} // Update the column unit value automatically only in create mode
// Update the column unit value automatically only in create mode shouldUpdateYAxisUnit={isNewDashboard}
shouldUpdateYAxisUnit={isNewDashboard} />
/> );
); })}
})}
</div>
</section> </section>
); );
} }

View File

@@ -56,6 +56,9 @@ describe('ContextLinks Component', () => {
/>, />,
); );
// Check that the component renders
expect(screen.getByText('Context Links')).toBeInTheDocument();
// Check that the add button is present // Check that the add button is present
expect( expect(
screen.getByRole('button', { name: /context link/i }), screen.getByRole('button', { name: /context link/i }),

View File

@@ -14,7 +14,7 @@ import {
verticalListSortingStrategy, verticalListSortingStrategy,
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { Button, Modal } from 'antd'; import { Button, Modal, Typography } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react'; import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
import { import {
@@ -134,16 +134,11 @@ function ContextLinks({
return ( return (
<div className="context-links-container"> <div className="context-links-container">
<Typography.Text className="context-links-text">
Context Links
</Typography.Text>
<div className="context-links-list"> <div className="context-links-list">
<Button
type="default"
className="add-context-link-button"
icon={<Plus size={12} />}
style={{ width: '100%' }}
onClick={handleAddContextLink}
>
Add Context Link
</Button>
<OverlayScrollbar> <OverlayScrollbar>
<DndContext <DndContext
sensors={sensors} sensors={sensors}
@@ -165,6 +160,16 @@ function ContextLinks({
</SortableContext> </SortableContext>
</DndContext> </DndContext>
</OverlayScrollbar> </OverlayScrollbar>
{/* button to add context link */}
<Button
type="primary"
className="add-context-link-button"
icon={<Plus size={12} />}
onClick={handleAddContextLink}
>
Context Link
</Button>
</div> </div>
<Modal <Modal

View File

@@ -2,6 +2,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
margin: 12px;
} }
.context-links-text { .context-links-text {
@@ -109,7 +110,10 @@
} }
.add-context-link-button { .add-context-link-button {
width: 100%; display: flex;
align-items: center;
margin: auto;
width: fit-content;
} }
.lightMode { .lightMode {

View File

@@ -1,7 +1,6 @@
.right-container { .right-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-bottom: 48px;
.header { .header {
display: flex; display: flex;
@@ -25,14 +24,14 @@
letter-spacing: -0.07px; letter-spacing: -0.07px;
} }
} }
.control-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.name-description { .name-description {
padding: 0 0 4px 0; display: flex;
flex-direction: column;
padding: 12px 12px 16px 12px;
border-top: 1px solid var(--bg-slate-500);
border-bottom: 1px solid var(--bg-slate-500);
gap: 8px;
.typography { .typography {
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
@@ -89,6 +88,9 @@
.panel-config { .panel-config {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 12px 12px 16px 12px;
gap: 8px;
border-bottom: 1px solid var(--bg-slate-500);
.typography { .typography {
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
@@ -102,7 +104,6 @@
} }
.panel-type-select { .panel-type-select {
width: 100%;
.ant-select-selector { .ant-select-selector {
display: flex; display: flex;
height: 32px; height: 32px;
@@ -136,6 +137,7 @@
} }
.fill-gaps { .fill-gaps {
margin-top: 16px;
display: flex; display: flex;
padding: 12px; padding: 12px;
justify-content: space-between; justify-content: space-between;
@@ -154,24 +156,31 @@
letter-spacing: 0.52px; letter-spacing: 0.52px;
text-transform: uppercase; text-transform: uppercase;
} }
.fill-gaps-text-description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
opacity: 0.6;
line-height: 16px; /* 133.333% */
}
} }
.log-scale, .log-scale,
.decimal-precision-selector, .decimal-precision-selector {
.legend-position { margin-top: 16px;
display: flex;
justify-content: space-between; justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.legend-position {
margin-top: 16px;
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.legend-colors {
margin-top: 16px;
} }
.panel-time-text { .panel-time-text {
margin-top: 16px;
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
font-family: 'Space Mono'; font-family: 'Space Mono';
font-size: 13px; font-size: 13px;
@@ -184,6 +193,7 @@
.y-axis-unit-selector, .y-axis-unit-selector,
.y-axis-unit-selector-v2 { .y-axis-unit-selector-v2 {
margin-top: 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
@@ -268,8 +278,11 @@
} }
.stack-chart { .stack-chart {
flex-direction: row; margin-top: 16px;
display: flex;
justify-content: space-between; justify-content: space-between;
gap: 8px;
.label { .label {
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
font-family: 'Space Mono'; font-family: 'Space Mono';
@@ -283,6 +296,11 @@
} }
.bucket-config { .bucket-config {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.label { .label {
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
font-family: 'Space Mono'; font-family: 'Space Mono';
@@ -334,13 +352,16 @@
} }
} }
.context-links {
border-bottom: 1px solid var(--bg-slate-500);
}
.alerts { .alerts {
display: flex; display: flex;
padding: 12px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 12px; border-bottom: 1px solid var(--bg-slate-500);
min-height: 44px;
border-top: 1px solid var(--bg-slate-500);
cursor: pointer; cursor: pointer;
.left-section { .left-section {
@@ -366,16 +387,6 @@
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
} }
} }
.context-links {
padding: 12px 12px 16px 12px;
border-bottom: 1px solid var(--bg-slate-500);
}
.thresholds-section {
padding: 12px 12px 16px 12px;
border-top: 1px solid var(--bg-slate-500);
}
} }
.select-option { .select-option {
@@ -407,6 +418,9 @@
} }
.name-description { .name-description {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.typography { .typography {
color: var(--bg-ink-400); color: var(--bg-ink-400);
} }
@@ -427,6 +441,8 @@
} }
.panel-config { .panel-config {
border-bottom: 1px solid var(--bg-vanilla-300);
.typography { .typography {
color: var(--bg-ink-400); color: var(--bg-ink-400);
} }
@@ -462,9 +478,6 @@
.fill-gaps-text { .fill-gaps-text {
color: var(--bg-ink-400); color: var(--bg-ink-400);
} }
.fill-gaps-text-description {
color: var(--bg-ink-400);
}
} }
.bucket-config { .bucket-config {
@@ -517,7 +530,7 @@
} }
.alerts { .alerts {
border-top: 1px solid var(--bg-vanilla-300); border-bottom: 1px solid var(--bg-vanilla-300);
.left-section { .left-section {
.bell-icon { .bell-icon {
@@ -536,10 +549,6 @@
.context-links { .context-links {
border-bottom: 1px solid var(--bg-vanilla-300); border-bottom: 1px solid var(--bg-vanilla-300);
} }
.thresholds-section {
border-top: 1px solid var(--bg-vanilla-300);
}
} }
.select-option { .select-option {

View File

@@ -1,4 +1,5 @@
.threshold-selector-container { .threshold-selector-container {
padding: 12px;
padding-bottom: 80px; padding-bottom: 80px;
.threshold-select { .threshold-select {

View File

@@ -1,10 +1,10 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import { Button } from 'antd'; import { Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryLabels } from 'hooks/useGetQueryLabels'; import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
import { Plus } from 'lucide-react'; import { Antenna, Plus } from 'lucide-react';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import Threshold from './Threshold'; import Threshold from './Threshold';
@@ -68,14 +68,11 @@ function ThresholdSelector({
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<div className="threshold-selector-container"> <div className="threshold-selector-container">
<div className="threshold-select" onClick={addThresholdHandler}> <div className="threshold-select" onClick={addThresholdHandler}>
<Button <div className="left-section">
type="default" <Antenna size={14} className="icon" />
icon={<Plus size={14} />} <Typography.Text className="text">Thresholds</Typography.Text>
style={{ width: '100%' }} </div>
onClick={addThresholdHandler} <Plus size={14} onClick={addThresholdHandler} className="icon" />
>
Add Threshold
</Button>
</div> </div>
{thresholds.map((threshold, idx) => ( {thresholds.map((threshold, idx) => (
<Threshold <Threshold

View File

@@ -1,68 +0,0 @@
.settings-section {
border-top: 1px solid var(--bg-slate-500);
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 12px 12px;
min-height: 44px;
background: transparent;
border: none;
outline: none;
cursor: pointer;
.settings-section-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-weight: 400;
text-transform: uppercase;
}
.chevron-icon {
color: var(--bg-vanilla-400);
transition: transform 0.2s ease-in-out;
&.open {
transform: rotate(180deg);
}
}
}
.settings-section-content {
padding: 0 12px 0 12px;
display: flex;
flex-direction: column;
gap: 20px;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.25s ease, opacity 0.25s ease, padding 0.25s ease;
&.open {
padding-bottom: 24px;
max-height: 1000px;
opacity: 1;
}
}
.lightMode {
.settings-section-header {
.chevron-icon {
color: var(--bg-ink-400);
}
.settings-section-title {
color: var(--bg-ink-400);
}
}
.settings-section {
border-top: 1px solid var(--bg-vanilla-300);
}
}

View File

@@ -1,51 +0,0 @@
import { ReactNode, useState } from 'react';
import { ChevronDown } from 'lucide-react';
import './SettingsSection.styles.scss';
export interface SettingsSectionProps {
title: string;
defaultOpen?: boolean;
children: ReactNode;
icon?: ReactNode;
}
function SettingsSection({
title,
defaultOpen = false,
children,
icon,
}: SettingsSectionProps): JSX.Element {
const [isOpen, setIsOpen] = useState(defaultOpen);
const toggleOpen = (): void => {
setIsOpen((prev) => !prev);
};
return (
<section className="settings-section">
<button
type="button"
className="settings-section-header"
onClick={toggleOpen}
>
<span className="settings-section-title">
{icon ? icon : null} {title}
</span>
<ChevronDown
size={16}
className={isOpen ? 'chevron-icon open' : 'chevron-icon'}
/>
</button>
<div
className={
isOpen ? 'settings-section-content open' : 'settings-section-content'
}
>
{children}
</div>
</section>
);
}
export default SettingsSection;

View File

@@ -14,6 +14,7 @@ import {
Input, Input,
InputNumber, InputNumber,
Select, Select,
Space,
Switch, Switch,
Typography, Typography,
} from 'antd'; } from 'antd';
@@ -27,16 +28,9 @@ import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts'; import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { import {
Antenna,
Axis3D,
ConciergeBell, ConciergeBell,
Layers,
LayoutDashboard,
LineChart, LineChart,
Link,
Pencil,
Plus, Plus,
SlidersHorizontal,
Spline, Spline,
SquareArrowOutUpRight, SquareArrowOutUpRight,
} from 'lucide-react'; } from 'lucide-react';
@@ -52,7 +46,6 @@ import { DataSource } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer'; import { popupContainer } from 'utils/selectPopupContainer';
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector'; import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
import SettingsSection from './components/SettingsSection/SettingsSection';
import { import {
panelTypeVsBucketConfig, panelTypeVsBucketConfig,
panelTypeVsColumnUnitPreferences, panelTypeVsColumnUnitPreferences,
@@ -185,21 +178,6 @@ function RightContainer({
})); }));
}, [dashboardVariables]); }, [dashboardVariables]);
const isAxisSectionVisible = useMemo(() => allowSoftMinMax || allowLogScale, [
allowSoftMinMax,
allowLogScale,
]);
const isFormattingSectionVisible = useMemo(
() => allowYAxisUnit || allowDecimalPrecision || allowPanelColumnPreference,
[allowYAxisUnit, allowDecimalPrecision, allowPanelColumnPreference],
);
const isLegendSectionVisible = useMemo(
() => allowLegendPosition || allowLegendColors,
[allowLegendPosition, allowLegendColors],
);
const updateCursorAndDropdown = (value: string, pos: number): void => { const updateCursorAndDropdown = (value: string, pos: number): void => {
setCursorPos(pos); setCursorPos(pos);
const lastDollar = value.lastIndexOf('$', pos - 1); const lastDollar = value.lastIndexOf('$', pos - 1);
@@ -215,15 +193,6 @@ function RightContainer({
}, 0); }, 0);
}; };
const decimapPrecisionOptions = useMemo(() => {
return [
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
];
}, []);
const handleInputCursor = (): void => { const handleInputCursor = (): void => {
const pos = inputRef.current?.input?.selectionStart ?? 0; const pos = inputRef.current?.input?.selectionStart ?? 0;
updateCursorAndDropdown(inputValue, pos); updateCursorAndDropdown(inputValue, pos);
@@ -294,297 +263,269 @@ function RightContainer({
<div className="right-container"> <div className="right-container">
<section className="header"> <section className="header">
<div className="purple-dot" /> <div className="purple-dot" />
<Typography.Text className="header-text">Panel Settings</Typography.Text> <Typography.Text className="header-text">Panel details</Typography.Text>
</section> </section>
<section className="name-description">
<SettingsSection title="General" defaultOpen icon={<Pencil size={14} />}> <Typography.Text className="typography">Name</Typography.Text>
<section className="name-description control-container"> <AutoComplete
<Typography.Text className="typography">Name</Typography.Text> options={dashboardVariableOptions}
<AutoComplete value={inputValue}
options={dashboardVariableOptions} onChange={onInputChange}
value={inputValue} onSelect={onSelect}
onChange={onInputChange} filterOption={filterOption}
onSelect={onSelect} style={{ width: '100%' }}
filterOption={filterOption} getPopupContainer={popupContainer}
style={{ width: '100%' }} placeholder="Enter the panel name here..."
getPopupContainer={popupContainer} open={autoCompleteOpen}
placeholder="Enter the panel name here..."
open={autoCompleteOpen}
>
<Input
rootClassName="name-input"
ref={inputRef}
onSelect={handleInputCursor}
onClick={handleInputCursor}
onBlur={(): void => setAutoCompleteOpen(false)}
/>
</AutoComplete>
<Typography.Text className="typography">Description</Typography.Text>
<TextArea
placeholder="Enter the panel description here..."
bordered
allowClear
value={description}
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}
rootClassName="description-input"
/>
</section>
</SettingsSection>
<section className="panel-config">
<SettingsSection
title="Visualization"
defaultOpen
icon={<LayoutDashboard size={14} />}
> >
<section className="panel-type control-container"> <Input
<Typography.Text className="typography">Panel Type</Typography.Text> rootClassName="name-input"
<Select ref={inputRef}
onChange={setGraphHandler} onSelect={handleInputCursor}
value={selectedGraph} onClick={handleInputCursor}
className="panel-type-select" onBlur={(): void => setAutoCompleteOpen(false)}
data-testid="panel-change-select" />
data-stacking-state={stackedBarChart ? 'true' : 'false'} </AutoComplete>
> <Typography.Text className="typography">Description</Typography.Text>
{graphTypes.map((item) => ( <TextArea
<Option key={item.name} value={item.name}> placeholder="Enter the panel description here..."
<div className="select-option"> bordered
<div className="icon">{item.icon}</div> allowClear
<Typography.Text className="display">{item.display}</Typography.Text> value={description}
</div> onChange={(event): void =>
</Option> onChangeHandler(setDescription, event.target.value)
))} }
</Select> rootClassName="description-input"
</section> />
</section>
{allowPanelTimePreference && ( <section className="panel-config">
<section className="panel-time-preference control-container"> <Typography.Text className="typography">Panel Type</Typography.Text>
<Typography.Text className="panel-time-text"> <Select
Panel Time Preference onChange={setGraphHandler}
</Typography.Text> value={selectedGraph}
<TimePreference style={{ width: '100%' }}
{...{ className="panel-type-select"
selectedTime, data-testid="panel-change-select"
setSelectedTime, data-stacking-state={stackedBarChart ? 'true' : 'false'}
}} >
/> {graphTypes.map((item) => (
</section> <Option key={item.name} value={item.name}>
)} <div className="select-option">
<div className="icon">{item.icon}</div>
{allowStackingBarChart && ( <Typography.Text className="display">{item.display}</Typography.Text>
<section className="stack-chart control-container">
<Typography.Text className="label">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
)}
{allowFillSpans && (
<section className="fill-gaps">
<div className="fill-gaps-text-container">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Typography.Text className="fill-gaps-text-description">
Fill gaps in data with 0 for continuity
</Typography.Text>
</div> </div>
<Switch </Option>
checked={isFillSpans} ))}
size="small" </Select>
onChange={(checked): void => setIsFillSpans(checked)}
{allowFillSpans && (
<Space className="fill-gaps">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Switch
checked={isFillSpans}
size="small"
onChange={(checked): void => setIsFillSpans(checked)}
/>
</Space>
)}
{allowPanelTimePreference && (
<>
<Typography.Text className="panel-time-text">
Panel Time Preference
</Typography.Text>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
/>
)}
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
)}
{allowDecimalPrecision && (
<section className="decimal-precision-selector">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<Select
options={[
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
{ label: '4 decimals', value: PrecisionOptionsEnum.FOUR },
{ label: 'Full Precision', value: PrecisionOptionsEnum.FULL },
]}
value={decimalPrecision}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
</section>
)}
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
<Typography.Text className="text">Soft Min</Typography.Text>
<InputNumber
type="number"
value={softMin}
onChange={softMinHandler}
rootClassName="input"
/> />
</section> </section>
)} <section className="container">
</SettingsSection> <Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
{isFormattingSectionVisible && ( value={softMax}
<SettingsSection type="number"
title="Formatting & Units" rootClassName="input"
icon={<SlidersHorizontal size={14} />} onChange={softMaxHandler}
>
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/> />
)} </section>
</section>
{allowDecimalPrecision && (
<section className="decimal-precision-selector control-container">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<Select
options={decimapPrecisionOptions}
value={decimalPrecision}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
</section>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
/>
)}
</SettingsSection>
)} )}
{isAxisSectionVisible && ( {allowStackingBarChart && (
<SettingsSection title="Axes" icon={<Axis3D size={14} />}> <section className="stack-chart">
{allowSoftMinMax && ( <Typography.Text className="label">Stack series</Typography.Text>
<section className="soft-min-max"> <Switch
<section className="container"> checked={stackedBarChart}
<Typography.Text className="text">Soft Min</Typography.Text> size="small"
<InputNumber onChange={(checked): void => setStackedBarChart(checked)}
type="number" />
value={softMin} </section>
onChange={softMinHandler}
rootClassName="input"
/>
</section>
<section className="container">
<Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
value={softMax}
type="number"
rootClassName="input"
onChange={softMaxHandler}
/>
</section>
</section>
)}
{allowLogScale && (
<section className="log-scale control-container">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Select
onChange={(value): void =>
setIsLogScale(value === LogScale.LOGARITHMIC)
}
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LogScale.LINEAR}
>
<Option value={LogScale.LINEAR}>
<div className="select-option">
<div className="icon">
<LineChart size={16} />
</div>
<Typography.Text className="display">Linear</Typography.Text>
</div>
</Option>
<Option value={LogScale.LOGARITHMIC}>
<div className="select-option">
<div className="icon">
<Spline size={16} />
</div>
<Typography.Text className="display">Logarithmic</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
</SettingsSection>
)}
{isLegendSectionVisible && (
<SettingsSection title="Legend" icon={<Layers size={14} />}>
{allowLegendPosition && (
<section className="legend-position control-container">
<Typography.Text className="typography">Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LegendPosition.BOTTOM}
>
<Option value={LegendPosition.BOTTOM}>
<div className="select-option">
<Typography.Text className="display">Bottom</Typography.Text>
</div>
</Option>
<Option value={LegendPosition.RIGHT}>
<div className="select-option">
<Typography.Text className="display">Right</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendColors && (
<section className="legend-colors">
<LegendColors
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
</section>
)}
</SettingsSection>
)} )}
{allowBucketConfig && ( {allowBucketConfig && (
<SettingsSection title="Histogram / Buckets"> <section className="bucket-config">
<section className="bucket-config control-container"> <Typography.Text className="label">Number of buckets</Typography.Text>
<Typography.Text className="label">Number of buckets</Typography.Text> <InputNumber
<InputNumber value={bucketCount || null}
value={bucketCount || null} type="number"
type="number" min={0}
min={0} rootClassName="bucket-input"
rootClassName="bucket-input" placeholder="Default: 30"
placeholder="Default: 30" onChange={(val): void => {
onChange={(val): void => { setBucketCount(val || 0);
setBucketCount(val || 0); }}
}} />
/> <Typography.Text className="label bucket-size-label">
<Typography.Text className="label bucket-size-label"> Bucket width
Bucket width </Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
/>
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text> </Typography.Text>
<InputNumber <Switch
value={bucketWidth || null} checked={combineHistogram}
type="number" size="small"
precision={2} onChange={(checked): void => setCombineHistogram(checked)}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
/> />
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text>
<Switch
checked={combineHistogram}
size="small"
onChange={(checked): void => setCombineHistogram(checked)}
/>
</section>
</section> </section>
</SettingsSection> </section>
)}
{allowLogScale && (
<section className="log-scale">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Select
onChange={(value): void => setIsLogScale(value === LogScale.LOGARITHMIC)}
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LogScale.LINEAR}
>
<Option value={LogScale.LINEAR}>
<div className="select-option">
<div className="icon">
<LineChart size={16} />
</div>
<Typography.Text className="display">Linear</Typography.Text>
</div>
</Option>
<Option value={LogScale.LOGARITHMIC}>
<div className="select-option">
<div className="icon">
<Spline size={16} />
</div>
<Typography.Text className="display">Logarithmic</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendPosition && (
<section className="legend-position">
<Typography.Text className="typography">Legend Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LegendPosition.BOTTOM}
>
<Option value={LegendPosition.BOTTOM}>
<div className="select-option">
<Typography.Text className="display">Bottom</Typography.Text>
</div>
</Option>
<Option value={LegendPosition.RIGHT}>
<div className="select-option">
<Typography.Text className="display">Right</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendColors && (
<section className="legend-colors">
<LegendColors
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
</section>
)} )}
</section> </section>
@@ -600,25 +541,17 @@ function RightContainer({
)} )}
{allowContextLinks && ( {allowContextLinks && (
<SettingsSection <section className="context-links">
title="Context Links"
icon={<Link size={14} />}
defaultOpen={!!contextLinks.linksData.length}
>
<ContextLinks <ContextLinks
contextLinks={contextLinks} contextLinks={contextLinks}
setContextLinks={setContextLinks} setContextLinks={setContextLinks}
selectedWidget={selectedWidget} selectedWidget={selectedWidget}
/> />
</SettingsSection> </section>
)} )}
{allowThreshold && ( {allowThreshold && (
<SettingsSection <section>
title="Thresholds"
icon={<Antenna size={14} />}
defaultOpen={!!thresholds.length}
>
<ThresholdSelector <ThresholdSelector
thresholds={thresholds} thresholds={thresholds}
setThresholds={setThresholds} setThresholds={setThresholds}
@@ -626,7 +559,7 @@ function RightContainer({
selectedGraph={selectedGraph} selectedGraph={selectedGraph}
columnUnits={columnUnits} columnUnits={columnUnits}
/> />
</SettingsSection> </section>
)} )}
</div> </div>
); );

View File

@@ -36,7 +36,7 @@ const checkStackSeriesState = (
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument(); expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
const stackSeriesSection = container.querySelector( const stackSeriesSection = container.querySelector(
'.stack-chart', 'section > .stack-chart',
) as HTMLElement; ) as HTMLElement;
expect(stackSeriesSection).toBeInTheDocument(); expect(stackSeriesSection).toBeInTheDocument();
@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
expect(getByText('Stack series')).toBeInTheDocument(); expect(getByText('Stack series')).toBeInTheDocument();
// Verify section exists // Verify section exists
const section = container.querySelector('.stack-chart'); const section = container.querySelector('section > .stack-chart');
expect(section).toBeInTheDocument(); expect(section).toBeInTheDocument();
// Verify switch is present and enabled (ant-switch-checked) // Verify switch is present and enabled (ant-switch-checked)

View File

@@ -439,19 +439,6 @@ function NewWidget({
globalSelectedInterval, globalSelectedInterval,
]); ]);
const navigateToDashboardPage = useCallback(() => {
const params = new URLSearchParams();
const urlVariablesQueryString = query.get(QueryParams.variables);
if (urlVariablesQueryString) {
params.set(QueryParams.variables, urlVariablesQueryString);
}
const search = params.toString() ? `?${params.toString()}` : '';
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }) + search);
}, [dashboardId, query, safeNavigate]);
const onClickSaveHandler = useCallback(() => { const onClickSaveHandler = useCallback(() => {
if (!selectedDashboard) { if (!selectedDashboard) {
return; return;
@@ -567,7 +554,9 @@ function NewWidget({
updateDashboardMutation.mutateAsync(dashboard, { updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: () => { onSuccess: () => {
setToScrollWidgetId(selectedWidget?.id || ''); setToScrollWidgetId(selectedWidget?.id || '');
navigateToDashboardPage(); safeNavigate({
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
});
}, },
}); });
}, [ }, [
@@ -583,7 +572,7 @@ function NewWidget({
updateDashboardMutation, updateDashboardMutation,
widgets, widgets,
setToScrollWidgetId, setToScrollWidgetId,
navigateToDashboardPage, safeNavigate,
dashboardId, dashboardId,
]); ]);
@@ -592,12 +581,12 @@ function NewWidget({
setDiscardModal(true); setDiscardModal(true);
return; return;
} }
navigateToDashboardPage(); safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [isQueryModified, navigateToDashboardPage]); }, [dashboardId, isQueryModified, safeNavigate]);
const discardChanges = useCallback(() => { const discardChanges = useCallback(() => {
navigateToDashboardPage(); safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [navigateToDashboardPage]); }, [dashboardId, safeNavigate]);
const setGraphHandler = (type: PANEL_TYPES): void => { const setGraphHandler = (type: PANEL_TYPES): void => {
setIsLoadingPanelData(true); setIsLoadingPanelData(true);
@@ -739,14 +728,12 @@ function NewWidget({
} }
const widgetId = query.get('widgetId') || ''; const widgetId = query.get('widgetId') || '';
const graphType = query.get('graphType') || ''; const graphType = query.get('graphType') || '';
const variables = query.get(QueryParams.variables) || '';
const queryParams = { const queryParams = {
[QueryParams.expandedWidgetId]: widgetId, [QueryParams.expandedWidgetId]: widgetId,
[QueryParams.graphType]: graphType, [QueryParams.graphType]: graphType,
[QueryParams.compositeQuery]: encodeURIComponent( [QueryParams.compositeQuery]: encodeURIComponent(
JSON.stringify(currentQuery), JSON.stringify(currentQuery),
), ),
[QueryParams.variables]: variables,
}; };
const updatedSearch = createQueryParams(queryParams); const updatedSearch = createQueryParams(queryParams);
@@ -835,54 +822,56 @@ function NewWidget({
</LeftContainerWrapper> </LeftContainerWrapper>
<RightContainerWrapper> <RightContainerWrapper>
<RightContainer <OverlayScrollbar>
setGraphHandler={setGraphHandler} <RightContainer
title={title} setGraphHandler={setGraphHandler}
setTitle={setTitle} title={title}
description={description} setTitle={setTitle}
setDescription={setDescription} description={description}
stackedBarChart={stackedBarChart} setDescription={setDescription}
setStackedBarChart={setStackedBarChart} stackedBarChart={stackedBarChart}
opacity={opacity} setStackedBarChart={setStackedBarChart}
yAxisUnit={yAxisUnit} opacity={opacity}
columnUnits={columnUnits} yAxisUnit={yAxisUnit}
setColumnUnits={setColumnUnits} columnUnits={columnUnits}
bucketCount={bucketCount} setColumnUnits={setColumnUnits}
bucketWidth={bucketWidth} bucketCount={bucketCount}
combineHistogram={combineHistogram} bucketWidth={bucketWidth}
setCombineHistogram={setCombineHistogram} combineHistogram={combineHistogram}
setBucketWidth={setBucketWidth} setCombineHistogram={setCombineHistogram}
setBucketCount={setBucketCount} setBucketWidth={setBucketWidth}
setOpacity={setOpacity} setBucketCount={setBucketCount}
selectedNullZeroValue={selectedNullZeroValue} setOpacity={setOpacity}
setSelectedNullZeroValue={setSelectedNullZeroValue} selectedNullZeroValue={selectedNullZeroValue}
selectedGraph={graphType} setSelectedNullZeroValue={setSelectedNullZeroValue}
setSelectedTime={setSelectedTime} selectedGraph={graphType}
selectedTime={selectedTime} setSelectedTime={setSelectedTime}
setYAxisUnit={setYAxisUnit} selectedTime={selectedTime}
decimalPrecision={decimalPrecision} setYAxisUnit={setYAxisUnit}
setDecimalPrecision={setDecimalPrecision} decimalPrecision={decimalPrecision}
thresholds={thresholds} setDecimalPrecision={setDecimalPrecision}
setThresholds={setThresholds} thresholds={thresholds}
selectedWidget={selectedWidget} setThresholds={setThresholds}
isFillSpans={isFillSpans} selectedWidget={selectedWidget}
setIsFillSpans={setIsFillSpans} isFillSpans={isFillSpans}
isLogScale={isLogScale} setIsFillSpans={setIsFillSpans}
setIsLogScale={setIsLogScale} isLogScale={isLogScale}
legendPosition={legendPosition} setIsLogScale={setIsLogScale}
setLegendPosition={setLegendPosition} legendPosition={legendPosition}
customLegendColors={customLegendColors} setLegendPosition={setLegendPosition}
setCustomLegendColors={setCustomLegendColors} customLegendColors={customLegendColors}
queryResponse={queryResponse} setCustomLegendColors={setCustomLegendColors}
softMin={softMin} queryResponse={queryResponse}
setSoftMin={setSoftMin} softMin={softMin}
softMax={softMax} setSoftMin={setSoftMin}
setSoftMax={setSoftMax} softMax={softMax}
contextLinks={contextLinks} setSoftMax={setSoftMax}
setContextLinks={setContextLinks} contextLinks={contextLinks}
enableDrillDown={enableDrillDown} setContextLinks={setContextLinks}
isNewDashboard={isNewDashboard} enableDrillDown={enableDrillDown}
/> isNewDashboard={isNewDashboard}
/>
</OverlayScrollbar>
</RightContainerWrapper> </RightContainerWrapper>
</PanelContainer> </PanelContainer>
<Modal <Modal

View File

@@ -15,14 +15,7 @@ export const RightContainerWrapper = styled(Col)`
overflow-y: auto; overflow-y: auto;
} }
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 0.3rem; width: 0rem;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
} }
`; `;

View File

@@ -1,339 +0,0 @@
import { renderHook } from '@testing-library/react';
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
jest.mock('hooks/dashboard/useDashboardFromLocalStorage');
jest.mock('hooks/dashboard/useVariablesFromUrl');
const mockUseDashboardVariablesFromLocalStorage = useDashboardVariablesFromLocalStorage as jest.MockedFunction<
typeof useDashboardVariablesFromLocalStorage
>;
const mockUseVariablesFromUrl = useVariablesFromUrl as jest.MockedFunction<
typeof useVariablesFromUrl
>;
const makeVariable = (
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable => ({
id: 'existing-id',
name: 'env',
description: '',
type: 'QUERY',
sort: 'DISABLED',
multiSelect: false,
showALLOption: false,
selectedValue: 'prod',
...overrides,
});
const makeDashboard = (
variables: Record<string, IDashboardVariable>,
): Dashboard => ({
id: 'dash-1',
createdAt: '',
updatedAt: '',
createdBy: '',
updatedBy: '',
data: {
title: 'Test',
variables,
},
});
const setupHook = (
currentDashboard: Record<string, any> = {},
urlVariables: Record<string, any> = {},
): ReturnType<typeof useTransformDashboardVariables> => {
mockUseDashboardVariablesFromLocalStorage.mockReturnValue({
currentDashboard,
updateLocalStorageDashboardVariables: jest.fn(),
});
mockUseVariablesFromUrl.mockReturnValue({
getUrlVariables: () => urlVariables,
setUrlVariables: jest.fn(),
updateUrlVariable: jest.fn(),
});
const { result } = renderHook(() => useTransformDashboardVariables('dash-1'));
return result.current;
};
describe('useTransformDashboardVariables', () => {
beforeEach(() => jest.clearAllMocks());
describe('order assignment', () => {
it('assigns order starting from 0 to variables that have none', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
});
const result = transformDashboardVariables(dashboard);
const orders = Object.values(result.data.variables).map((v) => v.order);
expect(orders).toContain(0);
expect(orders).toContain(1);
});
it('preserves existing order values', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'v1', order: 5 }),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.order).toBe(5);
});
it('assigns unique orders across multiple variables that all lack an order', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
v3: makeVariable({ id: 'id3', name: 'v3', order: undefined }),
});
const result = transformDashboardVariables(dashboard);
const orders = Object.values(result.data.variables).map((v) => v.order);
// All three newly assigned orders must be distinct
expect(new Set(orders).size).toBe(3);
});
});
describe('ID assignment', () => {
it('assigns a UUID to variables that have no id', () => {
const { transformDashboardVariables } = setupHook();
const variable = makeVariable({ name: 'v1' });
(variable as any).id = undefined;
const dashboard = makeDashboard({ v1: variable });
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
);
});
it('preserves existing IDs', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'keep-me', name: 'v1' }),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.id).toBe('keep-me');
});
});
describe('TEXTBOX backward compatibility', () => {
it('copies textboxValue to defaultValue when defaultValue is missing', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'v1',
type: 'TEXTBOX',
textboxValue: 'hello',
defaultValue: undefined,
order: undefined,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.defaultValue).toBe('hello');
});
it('does not overwrite an existing defaultValue', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'v1',
type: 'TEXTBOX',
textboxValue: 'old',
defaultValue: 'keep',
order: undefined,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.defaultValue).toBe('keep');
});
});
describe('localStorage merge', () => {
it('applies localStorage selectedValue over DB value', () => {
const { transformDashboardVariables } = setupHook({
env: { selectedValue: 'staging', allSelected: false },
});
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('staging');
});
it('applies localStorage allSelected over DB value', () => {
const { transformDashboardVariables } = setupHook({
env: { selectedValue: undefined, allSelected: true },
});
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
allSelected: false,
showALLOption: true,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.allSelected).toBe(true);
});
});
describe('URL variable override', () => {
it('sets allSelected=true when URL value is __ALL__', () => {
const { transformDashboardVariables } = setupHook(
{ env: { selectedValue: 'prod', allSelected: false } },
{ env: '__ALL__' },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
showALLOption: true,
allSelected: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.allSelected).toBe(true);
});
it('sets selectedValue from URL and clears allSelected when showALLOption is true', () => {
const { transformDashboardVariables } = setupHook(
{ env: { selectedValue: undefined, allSelected: true } },
{ env: 'dev' },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
showALLOption: true,
allSelected: true,
multiSelect: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('dev');
expect(result.data.variables.v1.allSelected).toBe(false);
});
it('does not set allSelected=false when showALLOption is false', () => {
const { transformDashboardVariables } = setupHook(
{ env: { selectedValue: undefined, allSelected: true } },
{ env: 'dev' },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
showALLOption: false,
allSelected: true,
multiSelect: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('dev');
expect(result.data.variables.v1.allSelected).toBe(true);
});
it('normalizes array URL value to single value for single-select variable', () => {
const { transformDashboardVariables } = setupHook(
{},
{ env: ['prod', 'dev'] },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
multiSelect: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('prod');
});
it('wraps single URL value in array for multi-select variable', () => {
const { transformDashboardVariables } = setupHook({}, { env: 'prod' });
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
multiSelect: true,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toEqual(['prod']);
});
it('looks up URL variable by variable id when name is absent', () => {
const { transformDashboardVariables } = setupHook(
{},
{ 'var-uuid': 'fallback' },
);
const variable = makeVariable({ id: 'var-uuid', multiSelect: false });
delete variable.name;
const dashboard = makeDashboard({ v1: variable });
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('fallback');
});
});
describe('edge cases', () => {
it('returns data unchanged when there are no variables', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables).toEqual({});
});
it('does not mutate the original dashboard', () => {
const { transformDashboardVariables } = setupHook({
env: { selectedValue: 'staging', allSelected: false },
});
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
});
const originalValue = dashboard.data.variables.v1.selectedValue;
transformDashboardVariables(dashboard);
expect(dashboard.data.variables.v1.selectedValue).toBe(originalValue);
});
});
});

View File

@@ -15,7 +15,7 @@ interface DashboardLocalStorageVariables {
[id: string]: LocalStoreDashboardVariables; [id: string]: LocalStoreDashboardVariables;
} }
export interface UseDashboardVariablesFromLocalStorageReturn { interface UseDashboardVariablesFromLocalStorageReturn {
currentDashboard: LocalStoreDashboardVariables; currentDashboard: LocalStoreDashboardVariables;
updateLocalStorageDashboardVariables: ( updateLocalStorageDashboardVariables: (
id: string, id: string,

View File

@@ -1,128 +0,0 @@
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import {
useDashboardVariablesFromLocalStorage,
UseDashboardVariablesFromLocalStorageReturn,
} from 'hooks/dashboard/useDashboardFromLocalStorage';
import useVariablesFromUrl, {
UseVariablesFromUrlReturn,
} from 'hooks/dashboard/useVariablesFromUrl';
import { isEmpty } from 'lodash-es';
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import { v4 as generateUUID } from 'uuid';
export function useTransformDashboardVariables(
dashboardId: string,
): Pick<UseVariablesFromUrlReturn, 'getUrlVariables' | 'updateUrlVariable'> &
UseDashboardVariablesFromLocalStorageReturn & {
transformDashboardVariables: (data: Dashboard) => Dashboard;
} {
const {
currentDashboard,
updateLocalStorageDashboardVariables,
} = useDashboardVariablesFromLocalStorage(dashboardId);
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
const mergeDBWithLocalStorage = (
data: Dashboard,
localStorageVariables: any,
): Dashboard => {
const updatedData = data;
if (data && localStorageVariables) {
const updatedVariables = data.data.variables;
const variablesFromUrl = getUrlVariables();
Object.keys(data.data.variables).forEach((variable) => {
const variableData = data.data.variables[variable];
// values from url
const urlVariable = variableData?.name
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
: variablesFromUrl[variableData.id];
let updatedVariable = {
...data.data.variables[variable],
...localStorageVariables[variableData.name as any],
};
// respect the url variable if it is set, override the others
if (!isEmpty(urlVariable)) {
if (urlVariable === ALL_SELECTED_VALUE) {
updatedVariable = {
...updatedVariable,
allSelected: true,
};
} else {
// Normalize URL value to match variable's multiSelect configuration
const normalizedValue = normalizeUrlValueForVariable(
urlVariable,
variableData,
);
updatedVariable = {
...updatedVariable,
selectedValue: normalizedValue,
// Only set allSelected to false if showALLOption is available
...(updatedVariable?.showALLOption && { allSelected: false }),
};
}
}
updatedVariables[variable] = updatedVariable;
});
updatedData.data.variables = updatedVariables;
}
return updatedData;
};
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
// eslint-disable-next-line sonarjs/cognitive-complexity
const transformDashboardVariables = (data: Dashboard): Dashboard => {
if (data && data.data && data.data.variables) {
const clonedDashboardData = mergeDBWithLocalStorage(
JSON.parse(JSON.stringify(data)),
currentDashboard,
);
const { variables } = clonedDashboardData.data;
const existingOrders: Set<number> = new Set();
for (const key in variables) {
// eslint-disable-next-line no-prototype-builtins
if (variables.hasOwnProperty(key)) {
const variable: IDashboardVariable = variables[key];
// Check if 'order' property doesn't exist or is undefined
if (variable.order === undefined) {
// Find a unique order starting from 0
let order = 0;
while (existingOrders.has(order)) {
order += 1;
}
variable.order = order;
existingOrders.add(order);
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
variable.defaultValue = variable.textboxValue || '';
}
}
if (variable.id === undefined) {
variable.id = generateUUID();
}
}
}
return clonedDashboardData;
}
return data;
};
return {
transformDashboardVariables,
getUrlVariables,
updateUrlVariable,
currentDashboard,
updateLocalStorageDashboardVariables,
};
}

View File

@@ -11,7 +11,7 @@ export interface LocalStoreDashboardVariables {
| IDashboardVariable['selectedValue']; | IDashboardVariable['selectedValue'];
} }
export interface UseVariablesFromUrlReturn { interface UseVariablesFromUrlReturn {
getUrlVariables: () => LocalStoreDashboardVariables; getUrlVariables: () => LocalStoreDashboardVariables;
setUrlVariables: (variables: LocalStoreDashboardVariables) => void; setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
updateUrlVariable: ( updateUrlVariable: (

View File

@@ -1,143 +0,0 @@
import { Route } from 'react-router-dom';
import * as getDashboardModule from 'api/v1/dashboards/id/get';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { rest, server } from 'mocks-server/server';
import { render, screen, waitFor } from 'tests/test-utils';
import DashboardWidget from '../index';
const DASHBOARD_ID = 'dash-1';
const WIDGET_ID = 'widget-abc';
const mockDashboardResponse = {
status: 'success',
data: {
id: DASHBOARD_ID,
createdAt: '2024-01-01T00:00:00Z',
createdBy: 'test',
updatedAt: '2024-01-01T00:00:00Z',
updatedBy: 'test',
isLocked: false,
data: {
collapsableRowsMigrated: true,
description: '',
name: '',
panelMap: {},
tags: [],
title: 'Test Dashboard',
uploadedGrafana: false,
uuid: '',
version: '',
variables: {},
widgets: [],
layout: [],
},
},
};
const mockSafeNavigate = jest.fn();
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: mockSafeNavigate,
}),
}));
jest.mock('container/NewWidget', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="new-widget">NewWidget</div>,
}));
// Wrap component in a Route so useParams can resolve dashboardId.
// Query params are passed via the URL so useUrlQuery (react-router) can read them.
function renderAtRoute(
queryState: Record<string, string | null> = {},
): ReturnType<typeof render> {
const params = new URLSearchParams();
Object.entries(queryState).forEach(([k, v]) => {
if (v !== null) {
params.set(k, v);
}
});
const search = params.toString() ? `?${params.toString()}` : '';
return render(
<Route path="/dashboard/:dashboardId/new">
<DashboardWidget />
</Route>,
undefined,
{ initialRoute: `/dashboard/${DASHBOARD_ID}/new${search}` },
);
}
beforeEach(() => {
mockSafeNavigate.mockClear();
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('DashboardWidget', () => {
it('redirects to dashboard when widgetId is missing', async () => {
renderAtRoute({ graphType: PANEL_TYPES.TIME_SERIES });
await waitFor(() => {
expect(mockSafeNavigate).toHaveBeenCalled();
});
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
});
it('redirects to dashboard when graphType is missing', async () => {
renderAtRoute({ widgetId: WIDGET_ID });
await waitFor(() => {
expect(mockSafeNavigate).toHaveBeenCalled();
});
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
});
it('shows spinner while dashboard is loading', () => {
// Spy instead of MSW delay('infinite') to avoid leaving an open network handle.
jest
.spyOn(getDashboardModule, 'default')
.mockReturnValue(new Promise(() => {}));
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
expect(screen.getByRole('img', { name: 'loading' })).toBeInTheDocument();
});
it('shows error message when dashboard fetch fails', async () => {
server.use(
rest.get(
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
(_req, res, ctx) => res(ctx.status(500), ctx.json({ status: 'error' })),
),
);
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
await waitFor(() => {
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
});
it('renders NewWidget when dashboard loads successfully', async () => {
server.use(
rest.get(
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(mockDashboardResponse)),
),
);
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
await waitFor(() => {
expect(screen.getByTestId('new-widget')).toBeInTheDocument();
});
});
});

View File

@@ -1,34 +1,29 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { generatePath, useParams } from 'react-router-dom'; import { generatePath, useParams } from 'react-router-dom';
import { Card, Typography } from 'antd'; import { Card, Typography } from 'antd';
import getDashboard from 'api/v1/dashboards/id/get'; import getDashboard from 'api/v1/dashboards/id/get';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime'; import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import NewWidget from 'container/NewWidget'; import NewWidget from 'container/NewWidget';
import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils'; import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery'; import { parseAsStringEnum, useQueryState } from 'nuqs';
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore'; import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import { Dashboard } from 'types/api/dashboard/getAll';
function DashboardWidget(): JSX.Element | null { function DashboardWidget(): JSX.Element | null {
const { dashboardId } = useParams<{ const { dashboardId } = useParams<{
dashboardId: string; dashboardId: string;
}>(); }>();
const query = useUrlQuery(); const [widgetId] = useQueryState('widgetId');
const { graphType, widgetId } = useMemo(() => { const [graphType] = useQueryState(
return { 'graphType',
graphType: query.get(QueryParams.graphType) as PANEL_TYPES, parseAsStringEnum<PANEL_TYPES>(Object.values(PANEL_TYPES)),
widgetId: query.get(QueryParams.widgetId), );
};
}, [query]);
const { safeNavigate } = useSafeNavigate(); const { safeNavigate } = useSafeNavigate();
@@ -62,15 +57,8 @@ function DashboardWidgetInternal({
widgetId: string; widgetId: string;
graphType: PANEL_TYPES; graphType: PANEL_TYPES;
}): JSX.Element | null { }): JSX.Element | null {
const [selectedDashboard, setSelectedDashboard] = useState<
Dashboard | undefined
>(undefined);
const { transformDashboardVariables } = useTransformDashboardVariables(
dashboardId,
);
const { const {
data: dashboardResponse,
isFetching: isFetchingDashboardResponse, isFetching: isFetchingDashboardResponse,
isError: isErrorDashboardResponse, isError: isErrorDashboardResponse,
} = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], { } = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], {
@@ -82,15 +70,17 @@ function DashboardWidgetInternal({
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
cacheTime: DASHBOARD_CACHE_TIME, cacheTime: DASHBOARD_CACHE_TIME,
onSuccess: (response) => { onSuccess: (response) => {
const updatedDashboardData = transformDashboardVariables(response.data);
setSelectedDashboard(updatedDashboardData);
setDashboardVariablesStore({ setDashboardVariablesStore({
dashboardId, dashboardId,
variables: updatedDashboardData.data.variables, variables: response.data.data.variables,
}); });
}, },
}); });
const selectedDashboard = useMemo(() => dashboardResponse?.data, [
dashboardResponse?.data,
]);
if (isFetchingDashboardResponse) { if (isFetchingDashboardResponse) {
return <Spinner tip="Loading.." />; return <Spinner tip="Loading.." />;
} }

View File

@@ -17,18 +17,21 @@ import { useDispatch, useSelector } from 'react-redux';
import { Modal } from 'antd'; import { Modal } from 'antd';
import getDashboard from 'api/v1/dashboards/id/get'; import getDashboard from 'api/v1/dashboards/id/get';
import locked from 'api/v1/dashboards/id/lock'; import locked from 'api/v1/dashboards/id/lock';
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables'; import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import useTabVisibility from 'hooks/useTabFocus'; import useTabVisibility from 'hooks/useTabFocus';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import { getMinMaxForSelectedTime } from 'lib/getMinMax'; import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import { defaultTo } from 'lodash-es'; import { defaultTo, isEmpty } from 'lodash-es';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
import isUndefined from 'lodash-es/isUndefined'; import isUndefined from 'lodash-es/isUndefined';
import omitBy from 'lodash-es/omitBy'; import omitBy from 'lodash-es/omitBy';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables'; import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
import { useErrorModal } from 'providers/ErrorModalProvider'; import { useErrorModal } from 'providers/ErrorModalProvider';
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
@@ -36,9 +39,10 @@ import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
import { SuccessResponseV2 } from 'types/api'; import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error'; import APIError from 'types/api/error';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid';
import { import {
DASHBOARD_CACHE_TIME, DASHBOARD_CACHE_TIME,
@@ -133,10 +137,9 @@ export function DashboardProvider({
const { const {
currentDashboard, currentDashboard,
updateLocalStorageDashboardVariables, updateLocalStorageDashboardVariables,
getUrlVariables, } = useDashboardVariablesFromLocalStorage(dashboardId);
updateUrlVariable,
transformDashboardVariables, const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
} = useTransformDashboardVariables(dashboardId);
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
const modalRef = useRef<any>(null); const modalRef = useRef<any>(null);
@@ -148,6 +151,99 @@ export function DashboardProvider({
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false); const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
const mergeDBWithLocalStorage = (
data: Dashboard,
localStorageVariables: any,
): Dashboard => {
const updatedData = data;
if (data && localStorageVariables) {
const updatedVariables = data.data.variables;
const variablesFromUrl = getUrlVariables();
Object.keys(data.data.variables).forEach((variable) => {
const variableData = data.data.variables[variable];
// values from url
const urlVariable = variableData?.name
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
: variablesFromUrl[variableData.id];
let updatedVariable = {
...data.data.variables[variable],
...localStorageVariables[variableData.name as any],
};
// respect the url variable if it is set, override the others
if (!isEmpty(urlVariable)) {
if (urlVariable === ALL_SELECTED_VALUE) {
updatedVariable = {
...updatedVariable,
allSelected: true,
};
} else {
// Normalize URL value to match variable's multiSelect configuration
const normalizedValue = normalizeUrlValueForVariable(
urlVariable,
variableData,
);
updatedVariable = {
...updatedVariable,
selectedValue: normalizedValue,
// Only set allSelected to false if showALLOption is available
...(updatedVariable?.showALLOption && { allSelected: false }),
};
}
}
updatedVariables[variable] = updatedVariable;
});
updatedData.data.variables = updatedVariables;
}
return updatedData;
};
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
// eslint-disable-next-line sonarjs/cognitive-complexity
const transformDashboardVariables = (data: Dashboard): Dashboard => {
if (data && data.data && data.data.variables) {
const clonedDashboardData = mergeDBWithLocalStorage(
JSON.parse(JSON.stringify(data)),
currentDashboard,
);
const { variables } = clonedDashboardData.data;
const existingOrders: Set<number> = new Set();
for (const key in variables) {
// eslint-disable-next-line no-prototype-builtins
if (variables.hasOwnProperty(key)) {
const variable: IDashboardVariable = variables[key];
// Check if 'order' property doesn't exist or is undefined
if (variable.order === undefined) {
// Find a unique order starting from 0
let order = 0;
while (existingOrders.has(order)) {
order += 1;
}
variable.order = order;
existingOrders.add(order);
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
variable.defaultValue = variable.textboxValue || '';
}
}
if (variable.id === undefined) {
variable.id = generateUUID();
}
}
}
return clonedDashboardData;
}
return data;
};
const dashboardResponse = useQuery( const dashboardResponse = useQuery(
[ [
REACT_QUERY_KEY.DASHBOARD_BY_ID, REACT_QUERY_KEY.DASHBOARD_BY_ID,
@@ -178,14 +274,13 @@ export function DashboardProvider({
}, },
onSuccess: (data: SuccessResponseV2<Dashboard>) => { onSuccess: (data: SuccessResponseV2<Dashboard>) => {
const updatedDashboardData = transformDashboardVariables(data?.data); // if the url variable is not set for any variable, set it to the default value
const variables = data?.data?.data?.variables;
// initialize URL variables after dashboard state is set to avoid race conditions
const variables = updatedDashboardData?.data?.variables;
if (variables) { if (variables) {
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable); initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
} }
const updatedDashboardData = transformDashboardVariables(data?.data);
const updatedDate = dayjs(updatedDashboardData?.updatedAt); const updatedDate = dayjs(updatedDashboardData?.updatedAt);
setIsDashboardLocked(updatedDashboardData?.locked || false); setIsDashboardLocked(updatedDashboardData?.locked || false);

View File

@@ -381,7 +381,6 @@ describe('Dashboard Provider - URL Variables Integration', () => {
multiSelect: false, multiSelect: false,
allSelected: false, allSelected: false,
showALLOption: true, showALLOption: true,
order: 0,
}, },
services: { services: {
id: 'svc-id', id: 'svc-id',
@@ -389,7 +388,6 @@ describe('Dashboard Provider - URL Variables Integration', () => {
multiSelect: true, multiSelect: true,
allSelected: false, allSelected: false,
showALLOption: true, showALLOption: true,
order: 1,
}, },
}, },
mockGetUrlVariables, mockGetUrlVariables,

View File

@@ -1,93 +0,0 @@
package cloudintegration
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
// CreateConnectionArtifact generates cloud provider specific connection information,
// client side handles how this information is shown
CreateConnectionArtifact(
ctx context.Context,
orgID valuer.UUID,
provider cloudintegrationtypes.CloudProviderType,
request *cloudintegrationtypes.ConnectionArtifactRequest,
) (*cloudintegrationtypes.ConnectionArtifact, error)
// GetAccountStatus returns agent connection status for a cloud integration account
GetAccountStatus(ctx context.Context, orgID, accountID valuer.UUID) (*cloudintegrationtypes.AccountStatus, error)
// ListConnectedAccounts lists accounts where agent is connected
ListConnectedAccounts(ctx context.Context, orgID valuer.UUID) (*cloudintegrationtypes.ConnectedAccounts, error)
// DisconnectAccount soft deletes/removes a cloud integration account.
DisconnectAccount(ctx context.Context, orgID, accountID valuer.UUID) error
// UpdateAccountConfig updates the configuration of an existing cloud account for a specific organization.
UpdateAccountConfig(
ctx context.Context,
orgID,
accountID valuer.UUID,
config *cloudintegrationtypes.UpdateAccountConfigRequest,
) (*cloudintegrationtypes.Account, error)
// ListServicesMetadata returns list of services metadata for a cloud provider attached with the integrationID.
// This just returns a summary of the service and not the whole service definition
ListServicesMetadata(ctx context.Context, orgID valuer.UUID, integrationID *valuer.UUID) (*cloudintegrationtypes.ServicesMetadata, error)
// GetService returns service definition details for a serviceID. This returns config and
// other details required to show in service details page on web client.
GetService(ctx context.Context, orgID valuer.UUID, integrationID *valuer.UUID, serviceID string) (*cloudintegrationtypes.Service, error)
// UpdateServiceConfig updates cloud integration service config
UpdateServiceConfig(
ctx context.Context,
orgID valuer.UUID,
serviceID string,
config *cloudintegrationtypes.UpdateServiceConfigRequest,
) (*cloudintegrationtypes.UpdateServiceConfigResponse, error)
// AgentCheckIn is called by agent to heartbeat and get latest config in response.
AgentCheckIn(
ctx context.Context,
orgID valuer.UUID,
req *cloudintegrationtypes.AgentCheckInRequest,
) (*cloudintegrationtypes.AgentCheckInResponse, error)
// GetDashboardByID returns dashboard JSON for a given dashboard id.
// this only returns the dashboard when the service (embedded in dashboard id) is enabled
// in the org for any cloud integration account
GetDashboardByID(ctx context.Context, orgID valuer.UUID, id string) (*dashboardtypes.Dashboard, error)
// GetAllDashboards returns list of dashboards across all connected cloud integration accounts
// for enabled services in the org. This list gets added to dashboard list page
GetAllDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
}
// CloudProvider is the interface each cloud provider must implement.
type CloudProvider interface {
CreateArtifact(
ctx context.Context,
orgID valuer.UUID,
request *cloudintegrationtypes.ConnectionArtifactRequest,
creds cloudintegrationtypes.SignozCredentials,
accountID valuer.UUID,
) (artifact *cloudintegrationtypes.ConnectionArtifact, err error)
}
type Handler interface {
AgentCheckIn(http.ResponseWriter, *http.Request)
GenerateConnectionArtifact(http.ResponseWriter, *http.Request)
ListConnectedAccounts(http.ResponseWriter, *http.Request)
GetAccountStatus(http.ResponseWriter, *http.Request)
ListServices(http.ResponseWriter, *http.Request)
GetServiceDetails(http.ResponseWriter, *http.Request)
UpdateAccountConfig(http.ResponseWriter, *http.Request)
UpdateServiceConfig(http.ResponseWriter, *http.Request)
DisconnectAccount(http.ResponseWriter, *http.Request)
}

View File

@@ -1,61 +0,0 @@
package implcloudintegration
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct{}
func NewModule() cloudintegration.Module {
return &module{}
}
func (m *module) CreateConnectionArtifact(_ context.Context, _ valuer.UUID, _ cloudintegrationtypes.CloudProviderType, _ *cloudintegrationtypes.ConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}
func (m *module) GetAccountStatus(_ context.Context, _, _ valuer.UUID) (*cloudintegrationtypes.AccountStatus, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}
func (m *module) ListConnectedAccounts(_ context.Context, _ valuer.UUID) (*cloudintegrationtypes.ConnectedAccounts, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}
func (m *module) DisconnectAccount(_ context.Context, _, _ valuer.UUID) error {
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}
func (m *module) UpdateAccountConfig(_ context.Context, _, _ valuer.UUID, _ *cloudintegrationtypes.UpdateAccountConfigRequest) (*cloudintegrationtypes.Account, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}
func (m *module) ListServicesSummary(_ context.Context, _ valuer.UUID, _ *valuer.UUID) (*cloudintegrationtypes.ServicesSummary, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}
func (m *module) GetService(_ context.Context, _ valuer.UUID, _ string, _ *valuer.UUID) (*cloudintegrationtypes.Service, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}
func (m *module) UpdateServiceConfig(_ context.Context, _ string, _ valuer.UUID, _ *cloudintegrationtypes.UpdateServiceConfigRequest) (*cloudintegrationtypes.ServiceSummary, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}
func (m *module) AgentCheckIn(_ context.Context, _ valuer.UUID, _ *cloudintegrationtypes.AgentCheckInRequest) (cloudintegrationtypes.AgentCheckInResponse, error) {
return cloudintegrationtypes.AgentCheckInResponse{}, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}
func (m *module) GetDashboardByID(_ context.Context, _ string, _ valuer.UUID) (*dashboardtypes.Dashboard, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}
func (m *module) GetAllDashboards(_ context.Context, _ valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
}

View File

@@ -1,118 +0,0 @@
package implcloudintegration
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
store sqlstore.SQLStore
}
func NewStore(sqlStore sqlstore.SQLStore) cloudintegrationtypes.Store {
return &store{store: sqlStore}
}
func (s *store) GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) {
account := new(cloudintegrationtypes.StorableCloudIntegration)
err := s.store.BunDB().NewSelect().Model(account).
Where("id = ?", id).
Where("org_id = ?", orgID).
Where("provider = ?", provider).
Scan(ctx)
if err != nil {
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration account with id %s not found", id)
}
return account, nil
}
func (s *store) UpsertAccount(ctx context.Context, account *cloudintegrationtypes.StorableCloudIntegration) error {
account.UpdatedAt = time.Now()
_, err := s.store.BunDBCtx(ctx).NewInsert().Model(account).
On("CONFLICT (id, provider, org_id) DO UPDATE").
Set("config = EXCLUDED.config").
Set("account_id = EXCLUDED.account_id").
Set("last_agent_report = EXCLUDED.last_agent_report").
Set("removed_at = EXCLUDED.removed_at").
Set("updated_at = EXCLUDED.updated_at").
Exec(ctx)
return err
}
func (s *store) RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) error {
_, err := s.store.BunDBCtx(ctx).NewUpdate().Model((*cloudintegrationtypes.StorableCloudIntegration)(nil)).
Set("removed_at = ?", time.Now()).
Where("id = ?", id).
Where("org_id = ?", orgID).
Where("provider = ?", provider).
Exec(ctx)
return err
}
func (s *store) GetConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) ([]*cloudintegrationtypes.StorableCloudIntegration, error) {
var accounts []*cloudintegrationtypes.StorableCloudIntegration
err := s.store.BunDB().NewSelect().Model(&accounts).
Where("org_id = ?", orgID).
Where("provider = ?", provider).
Where("removed_at IS NULL").
Where("account_id IS NOT NULL").
Where("last_agent_report IS NOT NULL").
Order("created_at ASC").
Scan(ctx)
if err != nil {
return nil, err
}
return accounts, nil
}
func (s *store) GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, providerAccountID string) (*cloudintegrationtypes.StorableCloudIntegration, error) {
account := new(cloudintegrationtypes.StorableCloudIntegration)
err := s.store.BunDB().NewSelect().Model(account).
Where("org_id = ?", orgID).
Where("provider = ?", provider).
Where("account_id = ?", providerAccountID).
Where("last_agent_report IS NOT NULL").
Where("removed_at IS NULL").
Scan(ctx)
if err != nil {
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "connected account with provider account id %s not found", providerAccountID)
}
return account, nil
}
func (s *store) GetServiceByType(ctx context.Context, cloudIntegrationID valuer.UUID, serviceType string) (*cloudintegrationtypes.StorableCloudIntegrationService, error) {
service := new(cloudintegrationtypes.StorableCloudIntegrationService)
err := s.store.BunDB().NewSelect().Model(service).
Where("cloud_integration_id = ?", cloudIntegrationID).
Where("type = ?", serviceType).
Scan(ctx)
if err != nil {
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration service with type %s not found", serviceType)
}
return service, nil
}
func (s *store) UpsertService(ctx context.Context, service *cloudintegrationtypes.StorableCloudIntegrationService) error {
service.UpdatedAt = time.Now()
_, err := s.store.BunDBCtx(ctx).NewInsert().Model(service).
On("CONFLICT (cloud_integration_id, type) DO UPDATE").
Set("config = EXCLUDED.config").
Set("updated_at = EXCLUDED.updated_at").
Exec(ctx)
return err
}
func (s *store) GetServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*cloudintegrationtypes.StorableCloudIntegrationService, error) {
var services []*cloudintegrationtypes.StorableCloudIntegrationService
err := s.store.BunDB().NewSelect().Model(&services).
Where("cloud_integration_id = ?", cloudIntegrationID).
Scan(ctx)
if err != nil {
return nil, err
}
return services, nil
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/SigNoz/signoz/pkg/factory/factorytest" "github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/flagger" "github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest" "github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization" "github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/user/impluser" "github.com/SigNoz/signoz/pkg/modules/user/impluser"
@@ -51,7 +50,7 @@ func TestNewHandlers(t *testing.T) {
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger) userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, implcloudintegration.NewModule()) modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter)
querierHandler := querier.NewHandler(providerSettings, nil, nil) querierHandler := querier.NewHandler(providerSettings, nil, nil)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil) handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil)

View File

@@ -12,7 +12,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex" "github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
"github.com/SigNoz/signoz/pkg/modules/authdomain" "github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain" "github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer" "github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer" "github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
@@ -52,25 +51,24 @@ import (
) )
type Modules struct { type Modules struct {
OrgGetter organization.Getter OrgGetter organization.Getter
OrgSetter organization.Setter OrgSetter organization.Setter
Preference preference.Module Preference preference.Module
User user.Module User user.Module
UserGetter user.Getter UserGetter user.Getter
SavedView savedview.Module SavedView savedview.Module
Apdex apdex.Module Apdex apdex.Module
Dashboard dashboard.Module Dashboard dashboard.Module
QuickFilter quickfilter.Module QuickFilter quickfilter.Module
TraceFunnel tracefunnel.Module TraceFunnel tracefunnel.Module
RawDataExport rawdataexport.Module RawDataExport rawdataexport.Module
AuthDomain authdomain.Module AuthDomain authdomain.Module
Session session.Module Session session.Module
Services services.Module Services services.Module
SpanPercentile spanpercentile.Module SpanPercentile spanpercentile.Module
MetricsExplorer metricsexplorer.Module MetricsExplorer metricsexplorer.Module
Promote promote.Module Promote promote.Module
ServiceAccount serviceaccount.Module ServiceAccount serviceaccount.Module
CloudIntegration cloudintegration.Module
} }
func NewModules( func NewModules(
@@ -91,7 +89,6 @@ func NewModules(
config Config, config Config,
dashboard dashboard.Module, dashboard dashboard.Module,
userGetter user.Getter, userGetter user.Getter,
cloudIntegration cloudintegration.Module,
) Modules { ) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore)) quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter) orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
@@ -99,24 +96,23 @@ func NewModules(
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings) ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
return Modules{ return Modules{
OrgGetter: orgGetter, OrgGetter: orgGetter,
OrgSetter: orgSetter, OrgSetter: orgSetter,
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()), Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
SavedView: implsavedview.NewModule(sqlstore), SavedView: implsavedview.NewModule(sqlstore),
Apdex: implapdex.NewModule(sqlstore), Apdex: implapdex.NewModule(sqlstore),
Dashboard: dashboard, Dashboard: dashboard,
User: user, User: user,
UserGetter: userGetter, UserGetter: userGetter,
QuickFilter: quickfilter, QuickFilter: quickfilter,
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)), TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
RawDataExport: implrawdataexport.NewModule(querier), RawDataExport: implrawdataexport.NewModule(querier),
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter), Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings), SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Services: implservices.NewModule(querier, telemetryStore), Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer), MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore), Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, providerSettings), ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, providerSettings),
CloudIntegration: cloudIntegration,
} }
} }

View File

@@ -13,7 +13,6 @@ import (
"github.com/SigNoz/signoz/pkg/factory/factorytest" "github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/flagger" "github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest" "github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization" "github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/user/impluser" "github.com/SigNoz/signoz/pkg/modules/user/impluser"
@@ -50,7 +49,7 @@ func TestNewModules(t *testing.T) {
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger) userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, implcloudintegration.NewModule()) modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter)
reflectVal := reflect.ValueOf(modules) reflectVal := reflect.ValueOf(modules)
for i := 0; i < reflectVal.NumField(); i++ { for i := 0; i < reflectVal.NumField(); i++ {

View File

@@ -18,7 +18,6 @@ import (
"github.com/SigNoz/signoz/pkg/gateway" "github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/instrumentation" "github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization" "github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
@@ -44,7 +43,6 @@ import (
"github.com/SigNoz/signoz/pkg/version" "github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus" "github.com/SigNoz/signoz/pkg/zeus"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/web" "github.com/SigNoz/signoz/pkg/web"
) )
@@ -93,7 +91,6 @@ func New(
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module, dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config], gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler, querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler,
cloudIntegrationModuleCallback func(sqlstore.SQLStore, licensing.Licensing, zeus.Zeus, gateway.Gateway, global.Config) cloudintegration.Module,
) (*SigNoz, error) { ) (*SigNoz, error) {
// Initialize instrumentation // Initialize instrumentation
instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz") instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz")
@@ -390,11 +387,8 @@ func New(
return nil, err return nil, err
} }
// Initialize cloudintegration module via callback
cloudIntegrationModule := cloudIntegrationModuleCallback(sqlstore, licensing, zeus, gateway, config.Global)
// Initialize all modules // Initialize all modules
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, cloudIntegrationModule) modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter)
userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root) userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root)

View File

@@ -1,49 +0,0 @@
package cloudintegrationtypes
import (
"time"
"github.com/SigNoz/signoz/pkg/valuer"
)
type (
ConnectedAccounts struct {
Accounts []*Account `json:"accounts"`
}
GettableConnectedAccounts = ConnectedAccounts
UpdateAccountConfigRequest struct {
AWS *AWSAccountConfig `json:"aws"`
}
UpdatableAccountConfig = UpdateAccountConfigRequest
)
type (
Account struct {
Id string `json:"id"`
ProviderAccountId *string `json:"providerAccountID,omitempty"`
Provider CloudProviderType `json:"provider"`
RemovedAt *time.Time `json:"removedAt,omitempty"`
AgentReport *AgentReport `json:"agentReport,omitempty"`
OrgID valuer.UUID `json:"orgID"`
Config *AccountConfig `json:"accountConfig,omitempty"`
}
GettableAccount = Account
)
// AgentReport represents heartbeats sent by the agent.
type AgentReport struct {
TimestampMillis int64 `json:"timestampMillis"`
Data map[string]any `json:"data"`
}
type AccountConfig struct {
AWS *AWSAccountConfig `json:"aws,omitempty"`
}
type AWSAccountConfig struct {
Regions []string `json:"regions"`
}

View File

@@ -1,82 +0,0 @@
package cloudintegrationtypes
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/uptrace/bun"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
ErrCodeCloudIntegrationNotFound = errors.MustNewCode("cloud_integration_not_found")
)
// StorableCloudIntegration represents a cloud integration stored in the database.
// This is also referred as "Account" in the context of cloud integrations.
type StorableCloudIntegration struct {
bun.BaseModel `bun:"table:cloud_integration"`
types.Identifiable
types.TimeAuditable
Provider CloudProviderType `json:"provider" bun:"provider,type:text"`
// Config is provider specific data in JSON string format
Config string `json:"config" bun:"config,type:text"`
AccountID *string `json:"account_id" bun:"account_id,type:text"`
LastAgentReport *StorableAgentReport `json:"last_agent_report" bun:"last_agent_report,type:text"`
RemovedAt *time.Time `json:"removed_at" bun:"removed_at,type:timestamp,nullzero"`
OrgID valuer.UUID `bun:"org_id,type:text"`
}
// StorableAgentReport represents the last heartbeat and arbitrary data sent by the agent
// as of now there is no use case for Data field, but keeping it for backwards compatibility with older structure.
type StorableAgentReport struct {
TimestampMillis int64 `json:"timestamp_millis"`
Data map[string]any `json:"data"`
}
// StorableCloudIntegrationService is to store service config for a cloud integration, which is a cloud provider specific configuration.
type StorableCloudIntegrationService struct {
bun.BaseModel `bun:"table:cloud_integration_service,alias:cis"`
types.Identifiable
types.TimeAuditable
Type valuer.String `bun:"type,type:text,notnull,unique:cloud_integration_id_type"`
// Config is cloud provider's service specific data in JSON string format
Config string `bun:"config,type:text"`
CloudIntegrationID valuer.UUID `bun:"cloud_integration_id,type:text,notnull,unique:cloud_integration_id_type,references:cloud_integration(id),on_delete:cascade"`
}
// Scan scans value from DB.
func (r *StorableAgentReport) Scan(src any) error {
var data []byte
switch v := src.(type) {
case []byte:
data = v
case string:
data = []byte(v)
default:
return errors.NewInternalf(errors.CodeInternal, "tried to scan from %T instead of string or bytes", src)
}
return json.Unmarshal(data, r)
}
// Value creates value to be stored in DB.
func (r *StorableAgentReport) Value() (driver.Value, error) {
if r == nil {
return nil, errors.NewInternalf(errors.CodeInternal, "agent report is nil")
}
serialized, err := json.Marshal(r)
if err != nil {
return nil, errors.WrapInternalf(
err, errors.CodeInternal, "couldn't serialize agent report to JSON",
)
}
// Return as string instead of []byte to ensure PostgreSQL stores as text, not bytes
return string(serialized), nil
}

View File

@@ -1,41 +0,0 @@
package cloudintegrationtypes
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
)
// CloudProviderType type alias.
type CloudProviderType struct{ valuer.String }
var (
// cloud providers.
CloudProviderTypeAWS = CloudProviderType{valuer.NewString("aws")}
CloudProviderTypeAzure = CloudProviderType{valuer.NewString("azure")}
// errors.
ErrCodeCloudProviderInvalidInput = errors.MustNewCode("invalid_cloud_provider")
AWSIntegrationUserEmail = valuer.MustNewEmail("aws-integration@signoz.io")
AzureIntegrationUserEmail = valuer.MustNewEmail("azure-integration@signoz.io")
)
// CloudIntegrationUserEmails is the list of valid emails for Cloud One Click integrations.
// This is used for validation and restrictions in different contexts, across codebase.
var CloudIntegrationUserEmails = []valuer.Email{
AWSIntegrationUserEmail,
AzureIntegrationUserEmail,
}
// NewCloudProvider returns a new CloudProviderType from a string.
// It validates the input and returns an error if the input is not valid cloud provider.
func NewCloudProvider(provider string) (CloudProviderType, error) {
switch provider {
case CloudProviderTypeAWS.StringValue():
return CloudProviderTypeAWS, nil
case CloudProviderTypeAzure.StringValue():
return CloudProviderTypeAzure, nil
default:
return CloudProviderType{}, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider)
}
}

View File

@@ -1,104 +0,0 @@
package cloudintegrationtypes
import "github.com/SigNoz/signoz/pkg/types/integrationtypes"
// request for creating connection artifact.
type (
PostableConnectionArtifact = ConnectionArtifactRequest
ConnectionArtifactRequest struct {
Aws *AWSConnectionArtifactRequest `json:"aws"`
}
AWSConnectionArtifactRequest struct {
DeploymentRegion string `json:"deploymentRegion"`
Regions []string `json:"regions"`
}
)
type (
// SignozCredentials is used to configure Agents to connect with Signoz
SignozCredentials struct {
SigNozAPIUrl string
SigNozAPIKey string // PAT
IngestionUrl string
IngestionKey string
}
ConnectionArtifact struct {
Aws *AWSConnectionArtifact `json:"aws"`
}
AWSConnectionArtifact struct {
ConnectionUrl string `json:"connectionURL"`
}
GettableConnectionArtifact = ConnectionArtifact
)
type (
AccountStatus struct {
Id string `json:"id"`
ProviderAccountId *string `json:"providerAccountID,omitempty"`
Status integrationtypes.AccountStatus `json:"status"`
}
GettableAccountStatus = AccountStatus
)
type (
AgentCheckInRequest struct {
// older backward compatible fields are mapped to new fields
// CloudIntegrationId string `json:"cloudIntegrationId"`
// AccountId string `json:"accountId"`
// New fields
ProviderAccountId string `json:"providerAccountId"`
CloudAccountId string `json:"cloudAccountId"`
Data map[string]any `json:"data,omitempty"`
}
PostableAgentCheckInRequest struct {
AgentCheckInRequest
// following are backward compatible fields for older running agents
// which gets mapped to new fields in AgentCheckInRequest
CloudIntegrationId string `json:"cloud_integration_id"`
CloudAccountId string `json:"cloud_account_id"`
}
GettableAgentCheckInResponse struct {
AgentCheckInResponse
CloudIntegrationId string `json:"cloud_integration_id"`
AccountId string `json:"account_id"`
}
AgentCheckInResponse struct {
// Older fields for backward compatibility are mapped to new fields below
// CloudIntegrationId string `json:"cloud_integration_id"`
// AccountId string `json:"account_id"`
// New fields
ProviderAccountId string `json:"providerAccountId"`
CloudAccountId string `json:"cloudAccountId"`
// IntegrationConfig populates data related to integration that is required for an agent
// to start collecting telemetry data
// keeping JSON key snake_case for backward compatibility
IntegrationConfig *IntegrationConfig `json:"integration_config,omitempty"`
}
IntegrationConfig struct {
EnabledRegions []string `json:"enabledRegions"` // backward compatible
Telemetry *AWSCollectionStrategy `json:"telemetry,omitempty"` // backward compatible
// new fields
AWS *AWSIntegrationConfig `json:"aws,omitempty"`
}
AWSIntegrationConfig struct {
EnabledRegions []string `json:"enabledRegions"`
Telemetry *AWSCollectionStrategy `json:"telemetry,omitempty"`
}
)

View File

@@ -1,103 +0,0 @@
package cloudintegrationtypes
import (
"github.com/SigNoz/signoz/pkg/errors"
)
var (
CodeInvalidCloudRegion = errors.MustNewCode("invalid_cloud_region")
CodeMismatchCloudProvider = errors.MustNewCode("cloud_provider_mismatch")
)
// List of all valid cloud regions on Amazon Web Services.
var ValidAWSRegions = map[string]struct{}{
"af-south-1": {}, // Africa (Cape Town).
"ap-east-1": {}, // Asia Pacific (Hong Kong).
"ap-northeast-1": {}, // Asia Pacific (Tokyo).
"ap-northeast-2": {}, // Asia Pacific (Seoul).
"ap-northeast-3": {}, // Asia Pacific (Osaka).
"ap-south-1": {}, // Asia Pacific (Mumbai).
"ap-south-2": {}, // Asia Pacific (Hyderabad).
"ap-southeast-1": {}, // Asia Pacific (Singapore).
"ap-southeast-2": {}, // Asia Pacific (Sydney).
"ap-southeast-3": {}, // Asia Pacific (Jakarta).
"ap-southeast-4": {}, // Asia Pacific (Melbourne).
"ca-central-1": {}, // Canada (Central).
"ca-west-1": {}, // Canada West (Calgary).
"eu-central-1": {}, // Europe (Frankfurt).
"eu-central-2": {}, // Europe (Zurich).
"eu-north-1": {}, // Europe (Stockholm).
"eu-south-1": {}, // Europe (Milan).
"eu-south-2": {}, // Europe (Spain).
"eu-west-1": {}, // Europe (Ireland).
"eu-west-2": {}, // Europe (London).
"eu-west-3": {}, // Europe (Paris).
"il-central-1": {}, // Israel (Tel Aviv).
"me-central-1": {}, // Middle East (UAE).
"me-south-1": {}, // Middle East (Bahrain).
"sa-east-1": {}, // South America (Sao Paulo).
"us-east-1": {}, // US East (N. Virginia).
"us-east-2": {}, // US East (Ohio).
"us-west-1": {}, // US West (N. California).
"us-west-2": {}, // US West (Oregon).
}
// List of all valid cloud regions for Microsoft Azure.
var ValidAzureRegions = map[string]struct{}{
"australiacentral": {}, // Australia Central
"australiacentral2": {}, // Australia Central 2
"australiaeast": {}, // Australia East
"australiasoutheast": {}, // Australia Southeast
"austriaeast": {}, // Austria East
"belgiumcentral": {}, // Belgium Central
"brazilsouth": {}, // Brazil South
"brazilsoutheast": {}, // Brazil Southeast
"canadacentral": {}, // Canada Central
"canadaeast": {}, // Canada East
"centralindia": {}, // Central India
"centralus": {}, // Central US
"chilecentral": {}, // Chile Central
"denmarkeast": {}, // Denmark East
"eastasia": {}, // East Asia
"eastus": {}, // East US
"eastus2": {}, // East US 2
"francecentral": {}, // France Central
"francesouth": {}, // France South
"germanynorth": {}, // Germany North
"germanywestcentral": {}, // Germany West Central
"indonesiacentral": {}, // Indonesia Central
"israelcentral": {}, // Israel Central
"italynorth": {}, // Italy North
"japaneast": {}, // Japan East
"japanwest": {}, // Japan West
"koreacentral": {}, // Korea Central
"koreasouth": {}, // Korea South
"malaysiawest": {}, // Malaysia West
"mexicocentral": {}, // Mexico Central
"newzealandnorth": {}, // New Zealand North
"northcentralus": {}, // North Central US
"northeurope": {}, // North Europe
"norwayeast": {}, // Norway East
"norwaywest": {}, // Norway West
"polandcentral": {}, // Poland Central
"qatarcentral": {}, // Qatar Central
"southafricanorth": {}, // South Africa North
"southafricawest": {}, // South Africa West
"southcentralus": {}, // South Central US
"southindia": {}, // South India
"southeastasia": {}, // Southeast Asia
"spaincentral": {}, // Spain Central
"swedencentral": {}, // Sweden Central
"switzerlandnorth": {}, // Switzerland North
"switzerlandwest": {}, // Switzerland West
"uaecentral": {}, // UAE Central
"uaenorth": {}, // UAE North
"uksouth": {}, // UK South
"ukwest": {}, // UK West
"westcentralus": {}, // West Central US
"westeurope": {}, // West Europe
"westindia": {}, // West India
"westus": {}, // West US
"westus2": {}, // West US 2
"westus3": {}, // West US 3
}

View File

@@ -1,213 +0,0 @@
package cloudintegrationtypes
import (
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var S3Sync = valuer.NewString("s3sync")
type (
ServicesMetadata struct {
Services []*ServiceMetadata `json:"services"`
}
// ServiceMetadata helps to quickly list available services and whether it is enabled or not.
// As getting complete service definition is a heavy operation and the response is also large,
// initial integration page load can be very slow.
ServiceMetadata struct {
ServiceDefinitionMetadata
// if the service is enabled for the account
Enabled bool `json:"enabled"`
}
GettableServicesMetadata = ServicesMetadata
Service struct {
ServiceDefinition
ServiceConfig *ServiceConfig `json:"serviceConfig"`
}
GettableService = Service
UpdateServiceConfigRequest struct {
CloudIntegrationId valuer.UUID `json:"cloudIntegrationId"`
ServiceConfig *ServiceConfig `json:"serviceConfig"`
}
UpdateServiceConfigResponse struct {
Id string `json:"id"` // service id
CloudIntegrationId valuer.UUID `json:"cloudIntegrationId"`
ServiceConfig *ServiceConfig `json:"serviceConfig"`
}
)
type ServiceConfig struct {
AWS *AWSServiceConfig `json:"aws,omitempty"`
}
type AWSServiceConfig struct {
Logs *AWSServiceLogsConfig `json:"logs"`
Metrics *AWSServiceMetricsConfig `json:"metrics"`
}
// AWSServiceLogsConfig is AWS specific logs config for a service
// NOTE: the JSON keys are snake case for backward compatibility with existing agents.
type AWSServiceLogsConfig struct {
Enabled bool `json:"enabled"`
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
}
type AWSServiceMetricsConfig struct {
Enabled bool `json:"enabled"`
}
// DefinitionMetadata represents service definition metadata. This is useful for showing service overview.
type ServiceDefinitionMetadata struct {
Id string `json:"id"`
Title string `json:"title"`
Icon string `json:"icon"`
}
type ServiceDefinition struct {
ServiceDefinitionMetadata
Overview string `json:"overview"` // markdown
Assets Assets `json:"assets"`
SupportedSignals SupportedSignals `json:"supported_signals"`
DataCollected DataCollected `json:"dataCollected"`
Strategy *CollectionStrategy `json:"telemetryCollectionStrategy"`
}
// CollectionStrategy is cloud provider specific configuration for signal collection,
// this is used by agent to understand the nitty-gritty for collecting telemetry for the cloud provider.
type CollectionStrategy struct {
AWS *AWSCollectionStrategy `json:"aws,omitempty"`
}
// Assets represents the collection of dashboards.
type Assets struct {
Dashboards []Dashboard `json:"dashboards"`
}
// SupportedSignals for cloud provider's service.
type SupportedSignals struct {
Logs bool `json:"logs"`
Metrics bool `json:"metrics"`
}
// DataCollected is curated static list of metrics and logs, this is shown as part of service overview.
type DataCollected struct {
Logs []CollectedLogAttribute `json:"logs"`
Metrics []CollectedMetric `json:"metrics"`
}
// CollectedLogAttribute represents a log attribute that is present in all log entries for a service,
// this is shown as part of service overview.
type CollectedLogAttribute struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
}
// CollectedMetric represents a metric that is collected for a service, this is shown as part of service overview.
type CollectedMetric struct {
Name string `json:"name"`
Type string `json:"type"`
Unit string `json:"unit"`
Description string `json:"description"`
}
// AWSCollectionStrategy represents signal collection strategy for AWS services.
// this is AWS specific.
// NOTE: this structure is still using snake case, for backward compatibility,
// with existing agents.
type AWSCollectionStrategy struct {
Metrics *AWSMetricsStrategy `json:"aws_metrics,omitempty"`
Logs *AWSLogsStrategy `json:"aws_logs,omitempty"`
S3Buckets map[string][]string `json:"s3_buckets,omitempty"` // Only available in S3 Sync Service Type in AWS
}
// AWSMetricsStrategy represents metrics collection strategy for AWS services.
// this is AWS specific.
// NOTE: this structure is still using snake case, for backward compatibility,
// with existing agents.
type AWSMetricsStrategy struct {
// to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters
StreamFilters []struct {
// json tags here are in the shape expected by AWS API as detailed at
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html
Namespace string `json:"Namespace"`
MetricNames []string `json:"MetricNames,omitempty"`
} `json:"cloudwatch_metric_stream_filters"`
}
// AWSLogsStrategy represents logs collection strategy for AWS services.
// this is AWS specific.
// NOTE: this structure is still using snake case, for backward compatibility,
// with existing agents.
type AWSLogsStrategy struct {
Subscriptions []struct {
// subscribe to all logs groups with specified prefix.
// eg: `/aws/rds/`
LogGroupNamePrefix string `json:"log_group_name_prefix"`
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
// "" implies no filtering is required.
FilterPattern string `json:"filter_pattern"`
} `json:"cloudwatch_logs_subscriptions"`
}
// Dashboard represents a dashboard definition for cloud integration.
// This is used to show available pre-made dashboards for a service,
// hence has additional fields like name and description and url for redirection to the right dashboard on click.
type Dashboard struct {
Id string `json:"id"`
Url string `json:"url"`
Title string `json:"title"`
Description string `json:"description"`
Image string `json:"image"`
Definition dashboardtypes.StorableDashboardData `json:"definition,omitempty"`
}
// UTILS
// GetCloudIntegrationDashboardID returns the dashboard id for a cloud integration, given the cloud provider, service id, and dashboard id.
// This is used to generate unique dashboard ids for cloud integration, and also to parse the dashboard id to get the cloud provider and service id when needed.
func GetCloudIntegrationDashboardID(cloudProvider CloudProviderType, svcId, dashboardId string) string {
return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId)
}
// GetDashboardsFromAssets returns the list of dashboards for the cloud provider service from definition.
func GetDashboardsFromAssets(
svcId string,
orgID valuer.UUID,
cloudProvider CloudProviderType,
createdAt time.Time,
assets Assets,
) []*dashboardtypes.Dashboard {
dashboards := make([]*dashboardtypes.Dashboard, 0)
for _, d := range assets.Dashboards {
author := fmt.Sprintf("%s-integration", cloudProvider)
dashboards = append(dashboards, &dashboardtypes.Dashboard{
ID: GetCloudIntegrationDashboardID(cloudProvider, svcId, d.Id),
Locked: true,
OrgID: orgID,
Data: d.Definition,
TimeAuditable: types.TimeAuditable{
CreatedAt: createdAt,
UpdatedAt: createdAt,
},
UserAuditable: types.UserAuditable{
CreatedBy: author,
UpdatedBy: author,
},
})
}
return dashboards
}

View File

@@ -1,41 +0,0 @@
package cloudintegrationtypes
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
// GetAccountByID returns a cloud integration account by id
GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) (*StorableCloudIntegration, error)
// CreateAccount creates a new cloud integration account
CreateAccount(ctx context.Context, orgID valuer.UUID, account *StorableCloudIntegration) (*StorableCloudIntegration, error)
// UpdateAccount updates an existing cloud integration account
UpdateAccount(ctx context.Context, account *StorableCloudIntegration) error
// RemoveAccount marks a cloud integration account as removed by setting the RemovedAt field
RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) error
// GetConnectedAccounts returns all the cloud integration accounts for the org and cloud provider
GetConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider CloudProviderType) ([]*StorableCloudIntegration, error)
// GetConnectedAccount for given provider
GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider CloudProviderType, providerAccountID string) (*StorableCloudIntegration, error)
// cloud_integration_service related methods
// GetServiceByType returns the cloud integration service for the given cloud integration id and service type
GetServiceByType(ctx context.Context, cloudIntegrationID valuer.UUID, serviceType string) (*StorableCloudIntegrationService, error)
// CreateService creates a new cloud integration service for the given cloud integration id and service type
CreateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *StorableCloudIntegrationService) (*StorableCloudIntegrationService, error)
// UpdateService updates an existing cloud integration service for the given cloud integration id and service type
UpdateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *StorableCloudIntegrationService) error
// GetServices returns all the cloud integration services for the given cloud integration id
GetServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*StorableCloudIntegrationService, error)
}

View File

@@ -1,10 +1,8 @@
package zeustypes package zeustypes
import ( import (
"fmt"
"net/url" "net/url"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
@@ -37,27 +35,6 @@ type Host struct {
URL string `json:"url" required:"true"` URL string `json:"url" required:"true"`
} }
// GettableDeployment represents the parsed deployment info from zeus.GetDeployment.
type GettableDeployment struct {
Name string
SignozAPIUrl string
}
// NewGettableDeployment parses raw GetDeployment bytes into a GettableDeployment.
func NewGettableDeployment(data []byte) (*GettableDeployment, error) {
parsed := gjson.ParseBytes(data)
name := parsed.Get("name").String()
dns := parsed.Get("cluster.region.dns").String()
if name == "" || dns == "" {
return nil, errors.NewInternalf(errors.CodeInternal,
"deployment info response missing name or cluster region dns")
}
return &GettableDeployment{
Name: name,
SignozAPIUrl: fmt.Sprintf("https://%s.%s", name, dns),
}, nil
}
func NewGettableHost(data []byte) *GettableHost { func NewGettableHost(data []byte) *GettableHost {
parsed := gjson.ParseBytes(data) parsed := gjson.ParseBytes(data)
dns := parsed.Get("cluster.region.dns").String() dns := parsed.Get("cluster.region.dns").String()