mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-13 00:33:17 +00:00
Compare commits
13 Commits
nv/10559
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c60019a6dc | ||
|
|
acde2a37fa | ||
|
|
0271be11e6 | ||
|
|
945241a52a | ||
|
|
92d220c4d9 | ||
|
|
0ed8169bad | ||
|
|
ed553fb02e | ||
|
|
47daba3c17 | ||
|
|
2b3310809a | ||
|
|
e967f80c86 | ||
|
|
234585e642 | ||
|
|
7281c36873 | ||
|
|
40288776e8 |
@@ -1768,19 +1768,19 @@ components:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
expires_at:
|
||||
expiresAt:
|
||||
minimum: 0
|
||||
type: integer
|
||||
id:
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
last_used:
|
||||
lastObservedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
service_account_id:
|
||||
serviceAccountId:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
@@ -1788,9 +1788,9 @@ components:
|
||||
required:
|
||||
- id
|
||||
- key
|
||||
- expires_at
|
||||
- last_used
|
||||
- service_account_id
|
||||
- expiresAt
|
||||
- lastObservedAt
|
||||
- serviceAccountId
|
||||
type: object
|
||||
ServiceaccounttypesGettableFactorAPIKeyWithKey:
|
||||
properties:
|
||||
@@ -1804,14 +1804,14 @@ components:
|
||||
type: object
|
||||
ServiceaccounttypesPostableFactorAPIKey:
|
||||
properties:
|
||||
expires_at:
|
||||
expiresAt:
|
||||
minimum: 0
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- expires_at
|
||||
- expiresAt
|
||||
type: object
|
||||
ServiceaccounttypesPostableServiceAccount:
|
||||
properties:
|
||||
@@ -1833,13 +1833,16 @@ components:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
deletedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
orgID:
|
||||
orgId:
|
||||
type: string
|
||||
roles:
|
||||
items:
|
||||
@@ -1856,18 +1859,19 @@ components:
|
||||
- email
|
||||
- roles
|
||||
- status
|
||||
- orgID
|
||||
- orgId
|
||||
- deletedAt
|
||||
type: object
|
||||
ServiceaccounttypesUpdatableFactorAPIKey:
|
||||
properties:
|
||||
expires_at:
|
||||
expiresAt:
|
||||
minimum: 0
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- expires_at
|
||||
- expiresAt
|
||||
type: object
|
||||
ServiceaccounttypesUpdatableServiceAccount:
|
||||
properties:
|
||||
|
||||
@@ -2,39 +2,45 @@ module base
|
||||
|
||||
type organisation
|
||||
relations
|
||||
define read: [user, role#assignee]
|
||||
define update: [user, role#assignee]
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
|
||||
type user
|
||||
relations
|
||||
define read: [user, role#assignee]
|
||||
define update: [user, role#assignee]
|
||||
define delete: [user, role#assignee]
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
|
||||
type serviceaccount
|
||||
relations
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
|
||||
type anonymous
|
||||
|
||||
type role
|
||||
relations
|
||||
define assignee: [user, anonymous]
|
||||
define assignee: [user, serviceaccount, anonymous]
|
||||
|
||||
define read: [user, role#assignee]
|
||||
define update: [user, role#assignee]
|
||||
define delete: [user, role#assignee]
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
|
||||
type metaresources
|
||||
relations
|
||||
define create: [user, role#assignee]
|
||||
define list: [user, role#assignee]
|
||||
define create: [user, serviceaccount, role#assignee]
|
||||
define list: [user, serviceaccount, role#assignee]
|
||||
|
||||
type metaresource
|
||||
relations
|
||||
define read: [user, anonymous, role#assignee]
|
||||
define update: [user, role#assignee]
|
||||
define delete: [user, role#assignee]
|
||||
define read: [user, serviceaccount, anonymous, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
define delete: [user, serviceaccount, role#assignee]
|
||||
|
||||
define block: [user, role#assignee]
|
||||
define block: [user, serviceaccount, role#assignee]
|
||||
|
||||
|
||||
type telemetryresource
|
||||
relations
|
||||
define read: [user, role#assignee]
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
14
ee/modules/cloudintegration/cloudintegration.go
Normal file
14
ee/modules/cloudintegration/cloudintegration.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package cloudintegration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
GetConnectionArtifact(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType)
|
||||
}
|
||||
|
||||
type CloudProvider interface{}
|
||||
@@ -128,6 +128,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
isAdmin &&
|
||||
(path === ROUTES.SETTINGS ||
|
||||
path === ROUTES.ORG_SETTINGS ||
|
||||
path === ROUTES.MEMBERS_SETTINGS ||
|
||||
path === ROUTES.BILLING ||
|
||||
path === ROUTES.MY_SETTINGS);
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ import posthog from 'posthog-js';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { IUser } from 'providers/App/types';
|
||||
import { CmdKProvider } from 'providers/cmdKProvider';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
@@ -321,6 +320,19 @@ function App(): JSX.Element {
|
||||
// Session Replay
|
||||
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
||||
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
||||
beforeSend(event) {
|
||||
const sessionReplayUrl = posthog.get_session_replay_url?.({
|
||||
withTimestamp: true,
|
||||
});
|
||||
if (sessionReplayUrl) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.contexts = {
|
||||
...event.contexts,
|
||||
posthog: { session_replay_url: sessionReplayUrl },
|
||||
};
|
||||
}
|
||||
return event;
|
||||
},
|
||||
});
|
||||
|
||||
setIsSentryInitialized(true);
|
||||
@@ -371,28 +383,26 @@ function App(): JSX.Element {
|
||||
<PrivateRoute>
|
||||
<ResourceProvider>
|
||||
<QueryBuilderProvider>
|
||||
<DashboardProvider>
|
||||
<KeyboardHotkeysProvider>
|
||||
<AppLayout>
|
||||
<PreferenceContextProvider>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</PreferenceContextProvider>
|
||||
</AppLayout>
|
||||
</KeyboardHotkeysProvider>
|
||||
</DashboardProvider>
|
||||
<KeyboardHotkeysProvider>
|
||||
<AppLayout>
|
||||
<PreferenceContextProvider>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</PreferenceContextProvider>
|
||||
</AppLayout>
|
||||
</KeyboardHotkeysProvider>
|
||||
</QueryBuilderProvider>
|
||||
</ResourceProvider>
|
||||
</PrivateRoute>
|
||||
|
||||
@@ -2100,7 +2100,7 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
expires_at: number;
|
||||
expiresAt: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2113,7 +2113,7 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
last_used: Date;
|
||||
lastObservedAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2121,7 +2121,7 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
service_account_id: string;
|
||||
serviceAccountId: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -2145,7 +2145,7 @@ export interface ServiceaccounttypesPostableFactorAPIKeyDTO {
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
expires_at: number;
|
||||
expiresAt: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2173,6 +2173,11 @@ export interface ServiceaccounttypesServiceAccountDTO {
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
deletedAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2188,7 +2193,7 @@ export interface ServiceaccounttypesServiceAccountDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgID: string;
|
||||
orgId: string;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
@@ -2209,7 +2214,7 @@ export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
expires_at: number;
|
||||
expiresAt: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -30,14 +30,15 @@ export default function CustomDomainEditModal({
|
||||
onClearError,
|
||||
onSubmit,
|
||||
}: CustomDomainEditModalProps): JSX.Element {
|
||||
const [value, setValue] = useState(customDomainSubdomain ?? '');
|
||||
const initialSubdomain = customDomainSubdomain ?? '';
|
||||
const [value, setValue] = useState(initialSubdomain);
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setValue(customDomainSubdomain ?? '');
|
||||
setValue(initialSubdomain);
|
||||
}
|
||||
}, [isOpen, customDomainSubdomain]);
|
||||
}, [isOpen, initialSubdomain]);
|
||||
|
||||
const handleClose = (): void => {
|
||||
setValidationError(null);
|
||||
@@ -58,6 +59,11 @@ export default function CustomDomainEditModal({
|
||||
};
|
||||
|
||||
const handleSubmit = (): void => {
|
||||
if (value === initialSubdomain) {
|
||||
setValidationError('Input is unchanged');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
setValidationError('This field is required');
|
||||
return;
|
||||
@@ -84,7 +90,7 @@ export default function CustomDomainEditModal({
|
||||
|
||||
const hasError = Boolean(errorMessage);
|
||||
|
||||
const statusIcon = ((): JSX.Element => {
|
||||
const statusIcon = ((): JSX.Element | null => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoaderCircle size={16} className="animate-spin edit-modal-status-icon" />
|
||||
@@ -95,7 +101,9 @@ export default function CustomDomainEditModal({
|
||||
return <CircleAlert size={16} color={Color.BG_CHERRY_500} />;
|
||||
}
|
||||
|
||||
return <CircleCheck size={16} color={Color.BG_FOREST_500} />;
|
||||
return value && value.length >= 3 ? (
|
||||
<CircleCheck size={16} color={Color.BG_FOREST_500} />
|
||||
) : null;
|
||||
})();
|
||||
|
||||
return (
|
||||
@@ -189,7 +197,7 @@ export default function CustomDomainEditModal({
|
||||
color="primary"
|
||||
className="edit-modal-apply-btn"
|
||||
onClick={handleSubmit}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || value === initialSubdomain}
|
||||
loading={isLoading}
|
||||
>
|
||||
Apply Changes
|
||||
|
||||
@@ -81,6 +81,10 @@
|
||||
padding-left: 26px;
|
||||
}
|
||||
|
||||
.custom-domain-card-meta-row.workspace-name-hidden {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.custom-domain-card-meta-timezone {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -117,32 +121,6 @@
|
||||
background: var(--l2-border);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.custom-domain-card-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-5);
|
||||
padding: var(--padding-3);
|
||||
}
|
||||
|
||||
.custom-domain-card-license {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
line-height: var(--line-height-20);
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.custom-domain-plan-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
background: var(--l2-background);
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
line-height: var(--line-height-20);
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-url-trigger {
|
||||
|
||||
@@ -69,8 +69,9 @@ function DomainUpdateToast({
|
||||
}
|
||||
|
||||
export default function CustomDomainSettings(): JSX.Element {
|
||||
const { org, activeLicense } = useAppContext();
|
||||
const { org } = useAppContext();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isPollingEnabled, setIsPollingEnabled] = useState(false);
|
||||
const [hosts, setHosts] = useState<ZeustypesHostDTO[] | null>(null);
|
||||
@@ -175,7 +176,8 @@ export default function CustomDomainSettings(): JSX.Element {
|
||||
[hosts, activeHost],
|
||||
);
|
||||
|
||||
const planName = activeLicense?.plan?.name;
|
||||
const workspaceName =
|
||||
org?.[0]?.displayName || customDomainSubdomain || activeHost?.name;
|
||||
|
||||
if (isLoadingHosts) {
|
||||
return (
|
||||
@@ -191,106 +193,98 @@ export default function CustomDomainSettings(): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="custom-domain-card">
|
||||
<div className="custom-domain-card-top">
|
||||
<div className="custom-domain-card-info">
|
||||
<div className="custom-domain-card-top">
|
||||
<div className="custom-domain-card-info">
|
||||
{!!workspaceName && (
|
||||
<div className="custom-domain-card-name-row">
|
||||
<span className="beacon" />
|
||||
<span className="custom-domain-card-org-name">
|
||||
{org?.[0]?.displayName ? org?.[0]?.displayName : customDomainSubdomain}
|
||||
</span>
|
||||
<span className="custom-domain-card-org-name">{workspaceName}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="custom-domain-card-meta-row">
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
dropdownRender={(): JSX.Element => (
|
||||
<div className="workspace-url-dropdown">
|
||||
<span className="workspace-url-dropdown-header">
|
||||
All Workspace URLs
|
||||
</span>
|
||||
<div className="workspace-url-dropdown-divider" />
|
||||
{sortedHosts.map((host) => {
|
||||
const isActive = host.name === activeHost?.name;
|
||||
return (
|
||||
<a
|
||||
key={host.name}
|
||||
href={host.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`workspace-url-dropdown-item${
|
||||
isActive ? ' workspace-url-dropdown-item--active' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="workspace-url-dropdown-item-label">
|
||||
{stripProtocol(host.url ?? '')}
|
||||
</span>
|
||||
{isActive ? (
|
||||
<Check size={14} className="workspace-url-dropdown-item-check" />
|
||||
) : (
|
||||
<ExternalLink
|
||||
size={12}
|
||||
className="workspace-url-dropdown-item-external"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
className="workspace-url-trigger"
|
||||
disabled={isFetchingHosts}
|
||||
>
|
||||
<Link2 size={12} />
|
||||
<span>{stripProtocol(activeHost?.url ?? '')}</span>
|
||||
<ChevronDown size={12} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<span className="custom-domain-card-meta-timezone">
|
||||
<Clock size={11} />
|
||||
{timezone.offset}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
className="custom-domain-edit-button"
|
||||
prefixIcon={<FilePenLine size={12} />}
|
||||
disabled={isFetchingHosts || isPollingEnabled}
|
||||
onClick={(): void => setIsEditModalOpen(true)}
|
||||
<div
|
||||
className={`custom-domain-card-meta-row ${
|
||||
!workspaceName ? 'workspace-name-hidden' : ''
|
||||
}`}
|
||||
>
|
||||
Edit workspace link
|
||||
</Button>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
dropdownRender={(): JSX.Element => (
|
||||
<div className="workspace-url-dropdown">
|
||||
<span className="workspace-url-dropdown-header">
|
||||
All Workspace URLs
|
||||
</span>
|
||||
<div className="workspace-url-dropdown-divider" />
|
||||
{sortedHosts.map((host) => {
|
||||
const isActive = host.name === activeHost?.name;
|
||||
return (
|
||||
<a
|
||||
key={host.name}
|
||||
href={host.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`workspace-url-dropdown-item${
|
||||
isActive ? ' workspace-url-dropdown-item--active' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="workspace-url-dropdown-item-label">
|
||||
{stripProtocol(host.url ?? '')}
|
||||
</span>
|
||||
{isActive ? (
|
||||
<Check size={14} className="workspace-url-dropdown-item-check" />
|
||||
) : (
|
||||
<ExternalLink
|
||||
size={12}
|
||||
className="workspace-url-dropdown-item-external"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
className="workspace-url-trigger"
|
||||
disabled={isFetchingHosts}
|
||||
>
|
||||
<Link2 size={12} />
|
||||
<span>{stripProtocol(activeHost?.url ?? '')}</span>
|
||||
<ChevronDown size={12} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<span className="custom-domain-card-meta-timezone">
|
||||
<Clock size={11} />
|
||||
{timezone.offset}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isPollingEnabled && (
|
||||
<Callout
|
||||
type="info"
|
||||
showIcon
|
||||
className="custom-domain-callout"
|
||||
size="small"
|
||||
icon={<SolidAlertCircle size={13} color="primary" />}
|
||||
message={`Updating your URL to ⎯ ${customDomainSubdomain}.${dnsSuffix}. This may take a few mins.`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="custom-domain-card-divider" />
|
||||
|
||||
<div className="custom-domain-card-bottom">
|
||||
<span className="beacon" />
|
||||
<span className="custom-domain-card-license">
|
||||
{planName && <code className="custom-domain-plan-badge">{planName}</code>}{' '}
|
||||
license is currently active
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
className="custom-domain-edit-button"
|
||||
prefixIcon={<FilePenLine size={12} />}
|
||||
disabled={isFetchingHosts || isPollingEnabled}
|
||||
onClick={(): void => setIsEditModalOpen(true)}
|
||||
>
|
||||
Edit workspace link
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isPollingEnabled && (
|
||||
<Callout
|
||||
type="info"
|
||||
showIcon
|
||||
className="custom-domain-callout"
|
||||
size="small"
|
||||
icon={<SolidAlertCircle size={13} color="primary" />}
|
||||
message={`Updating your URL to ⎯ ${customDomainSubdomain}.${dnsSuffix}. This may take a few mins.`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CustomDomainEditModal
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={(): void => setIsEditModalOpen(false)}
|
||||
|
||||
@@ -239,4 +239,87 @@ describe('CustomDomainSettings', () => {
|
||||
const { container } = render(toastRenderer('test-id'));
|
||||
expect(container).toHaveTextContent(/myteam\.test\.cloud/i);
|
||||
});
|
||||
|
||||
describe('Workspace Name rendering', () => {
|
||||
it('renders org displayName when available from appContext', async () => {
|
||||
server.use(
|
||||
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockHostsResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<CustomDomainSettings />, undefined, {
|
||||
appContextOverrides: {
|
||||
org: [{ id: 'xyz', displayName: 'My Org Name', createdAt: 0 }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(await screen.findByText('My Org Name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to customDomainSubdomain when org displayName is missing', async () => {
|
||||
server.use(
|
||||
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockHostsResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<CustomDomainSettings />, undefined, {
|
||||
appContextOverrides: { org: [] },
|
||||
});
|
||||
|
||||
expect(await screen.findByText('custom-host')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to activeHost.name when neither org name nor custom domain exists', async () => {
|
||||
const onlyDefaultHostResponse = {
|
||||
...mockHostsResponse,
|
||||
data: {
|
||||
...mockHostsResponse.data,
|
||||
hosts: mockHostsResponse.data.hosts
|
||||
? [mockHostsResponse.data.hosts[0]]
|
||||
: [],
|
||||
},
|
||||
};
|
||||
|
||||
server.use(
|
||||
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(onlyDefaultHostResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<CustomDomainSettings />, undefined, {
|
||||
appContextOverrides: { org: [] },
|
||||
});
|
||||
|
||||
// 'accepted-starfish' is the default host's name
|
||||
expect(await screen.findByText('accepted-starfish')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the card name row if workspaceName is totally falsy', async () => {
|
||||
const emptyHostsResponse = {
|
||||
...mockHostsResponse,
|
||||
data: {
|
||||
...mockHostsResponse.data,
|
||||
hosts: [],
|
||||
},
|
||||
};
|
||||
|
||||
server.use(
|
||||
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(emptyHostsResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
const { container } = render(<CustomDomainSettings />, undefined, {
|
||||
appContextOverrides: { org: [] },
|
||||
});
|
||||
|
||||
await screen.findByRole('button', { name: /edit workspace link/i });
|
||||
|
||||
expect(
|
||||
container.querySelector('.custom-domain-card-name-row'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,11 +34,6 @@ const mockSafeNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
useRouteMatch: jest.fn().mockReturnValue({
|
||||
params: {
|
||||
dashboardId: 4,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
@@ -69,7 +64,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider>
|
||||
<DashboardProvider dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
@@ -110,7 +105,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider>
|
||||
<DashboardProvider dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
@@ -149,7 +144,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider>
|
||||
<DashboardProvider dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
|
||||
@@ -10,6 +10,7 @@ import setRetentionApi from 'api/settings/setRetention';
|
||||
import setRetentionApiV2 from 'api/settings/setRetentionV2';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import CustomDomainSettings from 'container/CustomDomainSettings';
|
||||
import LicenseKeyRow from 'container/GeneralSettings/LicenseKeyRow/LicenseKeyRow';
|
||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -81,7 +82,7 @@ function GeneralSettings({
|
||||
logsTtlValuesPayload,
|
||||
);
|
||||
|
||||
const { user } = useAppContext();
|
||||
const { user, activeLicense } = useAppContext();
|
||||
|
||||
const [setRetentionPermission] = useComponentPermission(
|
||||
['set_retention_period'],
|
||||
@@ -680,7 +681,15 @@ function GeneralSettings({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{showCustomDomainSettings && <CustomDomainSettings />}
|
||||
{(showCustomDomainSettings || activeLicense?.key) && (
|
||||
<div className="custom-domain-card">
|
||||
{showCustomDomainSettings && <CustomDomainSettings />}
|
||||
{showCustomDomainSettings && activeLicense?.key && (
|
||||
<div className="custom-domain-card-divider" />
|
||||
)}
|
||||
{activeLicense?.key && <LicenseKeyRow />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="retention-controls-container">
|
||||
<div className="retention-controls-header">
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
.license-key-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--padding-2) var(--padding-3);
|
||||
gap: var(--spacing-5);
|
||||
|
||||
&__left {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--l2-foreground);
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
line-height: var(--line-height-20);
|
||||
letter-spacing: -0.07px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__value {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&__code {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 2px;
|
||||
border-radius: 2px 0 0 2px;
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'SF Mono', 'Fira Code', 'Fira Mono', monospace;
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
line-height: var(--line-height-20);
|
||||
white-space: nowrap;
|
||||
margin-right: -1px;
|
||||
}
|
||||
|
||||
&__copy-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
padding: 1px 2px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
color: var(--l2-foreground);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Copy, KeyRound } from '@signozhq/icons';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { getMaskedKey } from 'utils/maskedKey';
|
||||
|
||||
import './LicenseKeyRow.styles.scss';
|
||||
|
||||
function LicenseKeyRow(): JSX.Element | null {
|
||||
const { activeLicense } = useAppContext();
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
if (!activeLicense?.key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleCopyLicenseKey = (text: string): void => {
|
||||
copyToClipboard(text);
|
||||
toast.success('License key copied to clipboard.', { richColors: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="license-key-row">
|
||||
<span className="license-key-row__left">
|
||||
<KeyRound size={14} />
|
||||
<span className="license-key-row__label">SigNoz License Key</span>
|
||||
</span>
|
||||
<span className="license-key-row__value">
|
||||
<code className="license-key-row__code">
|
||||
{getMaskedKey(activeLicense.key)}
|
||||
</code>
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
aria-label="Copy license key"
|
||||
data-testid="license-key-row-copy-btn"
|
||||
className="license-key-row__copy-btn"
|
||||
onClick={(): void => handleCopyLicenseKey(activeLicense.key)}
|
||||
>
|
||||
<Copy size={12} />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LicenseKeyRow;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import LicenseKeyRow from '../LicenseKeyRow';
|
||||
|
||||
const mockCopyToClipboard = jest.fn();
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
__esModule: true,
|
||||
useCopyToClipboard: (): [unknown, jest.Mock] => [null, mockCopyToClipboard],
|
||||
}));
|
||||
|
||||
const mockToastSuccess = jest.fn();
|
||||
|
||||
jest.mock('@signozhq/sonner', () => ({
|
||||
toast: {
|
||||
success: (...args: unknown[]): unknown => mockToastSuccess(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('LicenseKeyRow', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders nothing when activeLicense key is absent', () => {
|
||||
const { container } = render(<LicenseKeyRow />, undefined, {
|
||||
appContextOverrides: { activeLicense: null },
|
||||
});
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders label and masked key when activeLicense key exists', () => {
|
||||
render(<LicenseKeyRow />, undefined, {
|
||||
appContextOverrides: {
|
||||
activeLicense: { key: 'abcdefghij' } as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText('SigNoz License Key')).toBeInTheDocument();
|
||||
expect(screen.getByText('ab·······ij')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls copyToClipboard and shows success toast when clipboard is available', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<LicenseKeyRow />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /copy license key/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('test-key');
|
||||
expect(mockToastSuccess).toHaveBeenCalledWith(
|
||||
'License key copied to clipboard.',
|
||||
{
|
||||
richColors: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,6 @@ import NewWidget from 'container/NewWidget';
|
||||
import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import i18n from 'ReactI18';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
@@ -104,15 +103,13 @@ describe('LogsPanelComponent', () => {
|
||||
const renderComponent = async (): Promise<void> => {
|
||||
render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<DashboardProvider>
|
||||
<PreferenceContextProvider>
|
||||
<NewWidget
|
||||
selectedGraph={PANEL_TYPES.LIST}
|
||||
fillSpans={undefined}
|
||||
yAxisUnit={undefined}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</DashboardProvider>
|
||||
<PreferenceContextProvider>
|
||||
<NewWidget
|
||||
dashboardId=""
|
||||
selectedDashboard={undefined}
|
||||
selectedGraph={PANEL_TYPES.LIST}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</I18nextProvider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Typography } from 'antd';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Copy } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { getMaskedKey } from 'utils/maskedKey';
|
||||
|
||||
import './LicenseSection.styles.scss';
|
||||
|
||||
@@ -12,15 +13,6 @@ function LicenseSection(): JSX.Element | null {
|
||||
const { notifications } = useNotifications();
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const getMaskedKey = (key: string): string => {
|
||||
if (!key || key.length < 4) {
|
||||
return key || 'N/A';
|
||||
}
|
||||
return `${key.substring(0, 2)}********${key
|
||||
.substring(key.length - 2)
|
||||
.trim()}`;
|
||||
};
|
||||
|
||||
const handleCopyKey = (text: string): void => {
|
||||
handleCopyToClipboard(text);
|
||||
notifications.success({
|
||||
|
||||
@@ -271,7 +271,7 @@ describe('MySettings Flows', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(within(container).getByText('ab********cd')).toBeInTheDocument();
|
||||
expect(within(container).getByText('ab·······cd')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not mask license key if it is too short', () => {
|
||||
|
||||
@@ -8,28 +8,15 @@ import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
import {
|
||||
getDefaultWidgetData,
|
||||
PANEL_TYPE_TO_QUERY_TYPES,
|
||||
} from 'container/NewWidget/utils';
|
||||
import { PANEL_TYPE_TO_QUERY_TYPES } from 'container/NewWidget/utils';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
// import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { defaultTo, isUndefined } from 'lodash-es';
|
||||
import { Atom, Terminal } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
getNextWidgets,
|
||||
getPreviousWidgets,
|
||||
getSelectedWidgetIndex,
|
||||
} from 'providers/Dashboard/util';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
||||
@@ -40,77 +27,25 @@ function QuerySection({
|
||||
selectedGraph,
|
||||
queryRangeKey,
|
||||
isLoadingQueries,
|
||||
selectedWidget,
|
||||
dashboardVersion,
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
isNewPanel,
|
||||
}: QueryProps): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
handleRunQuery: handleRunQueryFromQueryBuilder,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
const urlQuery = useUrlQuery();
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const { widgets } = selectedDashboard?.data || {};
|
||||
|
||||
const getWidget = useCallback(() => {
|
||||
const widgetId = urlQuery.get('widgetId');
|
||||
return defaultTo(
|
||||
widgets?.find((e) => e.id === widgetId),
|
||||
getDefaultWidgetData(widgetId || '', selectedGraph),
|
||||
);
|
||||
}, [urlQuery, widgets, selectedGraph]);
|
||||
|
||||
const selectedWidget = getWidget() as Widgets;
|
||||
|
||||
const { query } = selectedWidget;
|
||||
|
||||
useShareBuilderUrl({ defaultValue: query });
|
||||
|
||||
const handleStageQuery = useCallback(
|
||||
(query: Query): void => {
|
||||
if (selectedDashboard === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedWidgetIndex = getSelectedWidgetIndex(
|
||||
selectedDashboard,
|
||||
selectedWidget.id,
|
||||
);
|
||||
|
||||
const previousWidgets = getPreviousWidgets(
|
||||
selectedDashboard,
|
||||
selectedWidgetIndex,
|
||||
);
|
||||
|
||||
const nextWidgets = getNextWidgets(selectedDashboard, selectedWidgetIndex);
|
||||
|
||||
setSelectedDashboard({
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard?.data,
|
||||
widgets: [
|
||||
...previousWidgets,
|
||||
{
|
||||
...selectedWidget,
|
||||
query,
|
||||
},
|
||||
...nextWidgets,
|
||||
],
|
||||
},
|
||||
});
|
||||
handleRunQueryFromQueryBuilder();
|
||||
},
|
||||
[
|
||||
selectedDashboard,
|
||||
selectedWidget,
|
||||
setSelectedDashboard,
|
||||
handleRunQueryFromQueryBuilder,
|
||||
],
|
||||
);
|
||||
|
||||
const handleQueryCategoryChange = useCallback(
|
||||
(qCategory: string): void => {
|
||||
const currentQueryType = qCategory as EQueryType;
|
||||
@@ -123,19 +58,16 @@ function QuerySection({
|
||||
);
|
||||
|
||||
const handleRunQuery = (): void => {
|
||||
const widgetId = urlQuery.get('widgetId');
|
||||
const isNewPanel = isUndefined(widgets?.find((e) => e.id === widgetId));
|
||||
|
||||
logEvent('Panel Edit: Stage and run query', {
|
||||
dataSource: currentQuery.builder?.queryData?.[0]?.dataSource,
|
||||
panelType: selectedWidget.panelTypes,
|
||||
queryType: currentQuery.queryType,
|
||||
widgetId: selectedWidget.id,
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
isNewPanel,
|
||||
});
|
||||
handleStageQuery(currentQuery);
|
||||
handleRunQueryFromQueryBuilder();
|
||||
};
|
||||
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
@@ -164,7 +96,7 @@ function QuerySection({
|
||||
panelType={selectedGraph}
|
||||
filterConfigs={filterConfigs}
|
||||
showTraceOperator={selectedGraph !== PANEL_TYPES.LIST}
|
||||
version={selectedDashboard?.data?.version || 'v3'}
|
||||
version={dashboardVersion || 'v3'}
|
||||
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
||||
queryComponents={queryComponents}
|
||||
signalSourceChangeEnabled
|
||||
@@ -204,7 +136,7 @@ function QuerySection({
|
||||
queryComponents,
|
||||
selectedGraph,
|
||||
filterConfigs,
|
||||
selectedDashboard?.data?.version,
|
||||
dashboardVersion,
|
||||
isDarkMode,
|
||||
]);
|
||||
|
||||
@@ -261,6 +193,11 @@ interface QueryProps {
|
||||
selectedGraph: PANEL_TYPES;
|
||||
queryRangeKey?: QueryKey;
|
||||
isLoadingQueries?: boolean;
|
||||
selectedWidget: Widgets;
|
||||
dashboardVersion?: string;
|
||||
dashboardId?: string;
|
||||
dashboardName?: string;
|
||||
isNewPanel?: boolean;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
||||
|
||||
@@ -30,6 +30,8 @@ function LeftContainer({
|
||||
setRequestData,
|
||||
setQueryResponse,
|
||||
enableDrillDown = false,
|
||||
selectedDashboard,
|
||||
isNewPanel = false,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
@@ -75,6 +77,11 @@ function LeftContainer({
|
||||
selectedGraph={selectedGraph}
|
||||
queryRangeKey={queryRangeKey}
|
||||
isLoadingQueries={queryResponse.isFetching}
|
||||
selectedWidget={selectedWidget}
|
||||
dashboardVersion={ENTITY_VERSION_V5}
|
||||
dashboardId={selectedDashboard?.id}
|
||||
dashboardName={selectedDashboard?.data.title}
|
||||
isNewPanel={isNewPanel}
|
||||
/>
|
||||
{selectedGraph === PANEL_TYPES.LIST && (
|
||||
<ExplorerColumnsRenderer
|
||||
|
||||
@@ -8,7 +8,6 @@ import userEvent from '@testing-library/user-event';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { AppContext } from 'providers/App/App';
|
||||
import { IAppContext } from 'providers/App/types';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -96,9 +95,7 @@ const render = (ui: React.ReactElement): ReturnType<typeof rtlRender> =>
|
||||
<Provider store={createMockStore()}>
|
||||
<AppContext.Provider value={createMockAppContext() as IAppContext}>
|
||||
<ErrorModalProvider>
|
||||
<DashboardProvider>
|
||||
<QueryBuilderProvider>{ui}</QueryBuilderProvider>
|
||||
</DashboardProvider>
|
||||
<QueryBuilderProvider>{ui}</QueryBuilderProvider>
|
||||
</ErrorModalProvider>
|
||||
</AppContext.Provider>
|
||||
</Provider>
|
||||
|
||||
@@ -310,12 +310,12 @@ describe('Stacking bar in new panel', () => {
|
||||
|
||||
const { container, getByText } = render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<DashboardProvider>
|
||||
<DashboardProvider dashboardId="">
|
||||
<PreferenceContextProvider>
|
||||
<NewWidget
|
||||
dashboardId=""
|
||||
selectedDashboard={undefined}
|
||||
selectedGraph={PANEL_TYPES.BAR}
|
||||
fillSpans={undefined}
|
||||
yAxisUnit={undefined}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</DashboardProvider>
|
||||
@@ -356,11 +356,11 @@ describe('when switching to BAR panel type', () => {
|
||||
|
||||
it('should preserve saved stacking value of true', async () => {
|
||||
const { getByTestId, getByText, container } = render(
|
||||
<DashboardProvider>
|
||||
<DashboardProvider dashboardId="">
|
||||
<NewWidget
|
||||
dashboardId=""
|
||||
selectedDashboard={undefined}
|
||||
selectedGraph={PANEL_TYPES.BAR}
|
||||
fillSpans={undefined}
|
||||
yAxisUnit={undefined}
|
||||
/>
|
||||
</DashboardProvider>,
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath, useParams } from 'react-router-dom';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Modal, Space, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -32,8 +32,6 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
|
||||
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
|
||||
import {
|
||||
clearSelectedRowWidgetId,
|
||||
@@ -83,6 +81,8 @@ import {
|
||||
import './NewWidget.styles.scss';
|
||||
|
||||
function NewWidget({
|
||||
selectedDashboard,
|
||||
dashboardId,
|
||||
selectedGraph,
|
||||
enableDrillDown = false,
|
||||
}: NewWidgetProps): JSX.Element {
|
||||
@@ -90,11 +90,6 @@ function NewWidget({
|
||||
const setToScrollWidgetId = useScrollToWidgetIdStore(
|
||||
(s) => s.setToScrollWidgetId,
|
||||
);
|
||||
const {
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
columnWidths,
|
||||
} = useDashboard();
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
@@ -139,8 +134,6 @@ function NewWidget({
|
||||
|
||||
const query = useUrlQuery();
|
||||
|
||||
const { dashboardId } = useParams<DashboardWidgetPageParams>();
|
||||
|
||||
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
@@ -286,11 +279,10 @@ function NewWidget({
|
||||
isLogScale,
|
||||
legendPosition,
|
||||
customLegendColors,
|
||||
columnWidths: columnWidths?.[selectedWidget?.id],
|
||||
columnWidths: selectedWidget.columnWidths,
|
||||
contextLinks,
|
||||
};
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
columnUnits,
|
||||
currentQuery,
|
||||
@@ -313,8 +305,8 @@ function NewWidget({
|
||||
isLogScale,
|
||||
legendPosition,
|
||||
customLegendColors,
|
||||
columnWidths,
|
||||
contextLinks,
|
||||
selectedWidget.columnWidths,
|
||||
]);
|
||||
|
||||
const closeModal = (): void => {
|
||||
@@ -560,8 +552,7 @@ function NewWidget({
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(dashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
onSuccess: () => {
|
||||
setToScrollWidgetId(selectedWidget?.id || '');
|
||||
safeNavigate({
|
||||
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
||||
@@ -580,7 +571,6 @@ function NewWidget({
|
||||
preWidgets,
|
||||
updateDashboardMutation,
|
||||
widgets,
|
||||
setSelectedDashboard,
|
||||
setToScrollWidgetId,
|
||||
safeNavigate,
|
||||
dashboardId,
|
||||
@@ -630,22 +620,25 @@ function NewWidget({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query]);
|
||||
|
||||
const onSaveDashboard = useCallback((): void => {
|
||||
const isNewPanel = useMemo(() => {
|
||||
const widgetId = query.get('widgetId');
|
||||
const selectWidget = widgets?.find((e) => e.id === widgetId);
|
||||
const selectedWidget = widgets?.find((e) => e.id === widgetId);
|
||||
return isUndefined(selectedWidget);
|
||||
}, [query, widgets]);
|
||||
|
||||
const onSaveDashboard = useCallback((): void => {
|
||||
logEvent('Panel Edit: Save changes', {
|
||||
panelType: selectedWidget.panelTypes,
|
||||
dashboardId: selectedDashboard?.id,
|
||||
widgetId: selectedWidget.id,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
queryType: currentQuery.queryType,
|
||||
isNewPanel: isUndefined(selectWidget),
|
||||
isNewPanel,
|
||||
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
|
||||
});
|
||||
setSaveModal(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [isNewPanel]);
|
||||
|
||||
const isNewTraceLogsAvailable =
|
||||
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||
@@ -821,6 +814,8 @@ function NewWidget({
|
||||
isLoadingPanelData={isLoadingPanelData}
|
||||
setQueryResponse={setQueryResponse}
|
||||
enableDrillDown={enableDrillDown}
|
||||
selectedDashboard={selectedDashboard}
|
||||
isNewPanel={isNewPanel}
|
||||
/>
|
||||
)}
|
||||
</OverlayScrollbar>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Dispatch, SetStateAction } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { IDashboardContext } from 'providers/Dashboard/types';
|
||||
import { SuccessResponse, Warning } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
@@ -9,9 +10,9 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { timePreferance } from './RightContainer/timeItems';
|
||||
|
||||
export interface NewWidgetProps {
|
||||
dashboardId: string;
|
||||
selectedDashboard: IDashboardContext['selectedDashboard'];
|
||||
selectedGraph: PANEL_TYPES;
|
||||
yAxisUnit: Widgets['yAxisUnit'];
|
||||
fillSpans: Widgets['fillSpans'];
|
||||
enableDrillDown?: boolean;
|
||||
}
|
||||
|
||||
@@ -34,6 +35,8 @@ export interface WidgetGraphProps {
|
||||
>
|
||||
>;
|
||||
enableDrillDown?: boolean;
|
||||
selectedDashboard: IDashboardContext['selectedDashboard'];
|
||||
isNewPanel?: boolean;
|
||||
}
|
||||
|
||||
export type WidgetGraphContainerProps = {
|
||||
|
||||
@@ -38,6 +38,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
|
||||
[ROUTES.MY_SETTINGS]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.NOT_FOUND]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.ORG_SETTINGS]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.MEMBERS_SETTINGS]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.PASSWORD_RESET]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.SETTINGS]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.SIGN_UP]: [QueryParams.resourceAttributes],
|
||||
|
||||
@@ -107,7 +107,6 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
queryRangeMutation,
|
||||
dashboardVariables,
|
||||
dashboardDynamicVariables,
|
||||
selectedDashboard?.data.version,
|
||||
widget,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -23,10 +23,10 @@ export default {
|
||||
relations: {
|
||||
assignee: ['role'],
|
||||
create: ['metaresources'],
|
||||
delete: ['user', 'role', 'organization', 'metaresource'],
|
||||
delete: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'],
|
||||
list: ['metaresources'],
|
||||
read: ['user', 'role', 'organization', 'metaresource'],
|
||||
update: ['user', 'role', 'organization', 'metaresource'],
|
||||
read: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'],
|
||||
update: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
|
||||
import DashboardPage from './DashboardPage';
|
||||
|
||||
export default DashboardPage;
|
||||
function DashboardPageWithProvider(): JSX.Element {
|
||||
const { dashboardId } = useParams<{ dashboardId: string }>();
|
||||
|
||||
return (
|
||||
<DashboardProvider dashboardId={dashboardId}>
|
||||
<DashboardPage />
|
||||
</DashboardProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPageWithProvider;
|
||||
|
||||
@@ -1,50 +1,91 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { generatePath, useLocation, useParams } from 'react-router-dom';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { generatePath, useParams } from 'react-router-dom';
|
||||
import { Card, Typography } from 'antd';
|
||||
import getDashboard from 'api/v1/dashboards/id/get';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import NewWidget from 'container/NewWidget';
|
||||
import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
|
||||
function DashboardWidget(): JSX.Element | null {
|
||||
const { search } = useLocation();
|
||||
const { dashboardId } = useParams<DashboardWidgetPageParams>();
|
||||
const { dashboardId } = useParams<{
|
||||
dashboardId: string;
|
||||
}>();
|
||||
const [widgetId] = useQueryState('widgetId');
|
||||
const [graphType] = useQueryState(
|
||||
'graphType',
|
||||
parseAsStringEnum<PANEL_TYPES>(Object.values(PANEL_TYPES)),
|
||||
);
|
||||
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const [selectedGraph, setSelectedGraph] = useState<PANEL_TYPES>();
|
||||
|
||||
const { selectedDashboard, dashboardResponse } = useDashboard();
|
||||
|
||||
const params = useUrlQuery();
|
||||
|
||||
const widgetId = params.get('widgetId');
|
||||
const { data } = selectedDashboard || {};
|
||||
const { widgets } = data || {};
|
||||
|
||||
const selectedWidget = widgets?.find((e) => e.id === widgetId) as Widgets;
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(search);
|
||||
const graphType = params.get('graphType') as PANEL_TYPES | null;
|
||||
|
||||
if (graphType === null) {
|
||||
if (!graphType || !widgetId) {
|
||||
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
||||
} else {
|
||||
setSelectedGraph(graphType);
|
||||
} else if (!dashboardId) {
|
||||
safeNavigate(ROUTES.HOME);
|
||||
}
|
||||
}, [dashboardId, safeNavigate, search]);
|
||||
}, [graphType, widgetId, dashboardId, safeNavigate]);
|
||||
|
||||
if (selectedGraph === undefined || dashboardResponse.isLoading) {
|
||||
if (!widgetId || !graphType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardWidgetInternal
|
||||
dashboardId={dashboardId}
|
||||
widgetId={widgetId}
|
||||
graphType={graphType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardWidgetInternal({
|
||||
dashboardId,
|
||||
widgetId,
|
||||
graphType,
|
||||
}: {
|
||||
dashboardId: string;
|
||||
widgetId: string;
|
||||
graphType: PANEL_TYPES;
|
||||
}): JSX.Element | null {
|
||||
const {
|
||||
data: dashboardResponse,
|
||||
isFetching: isFetchingDashboardResponse,
|
||||
isError: isErrorDashboardResponse,
|
||||
} = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], {
|
||||
enabled: true,
|
||||
queryFn: async () =>
|
||||
await getDashboard({
|
||||
id: dashboardId,
|
||||
}),
|
||||
refetchOnWindowFocus: false,
|
||||
cacheTime: DASHBOARD_CACHE_TIME,
|
||||
onSuccess: (response) => {
|
||||
setDashboardVariablesStore({
|
||||
dashboardId,
|
||||
variables: response.data.data.variables,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const selectedDashboard = useMemo(() => dashboardResponse?.data, [
|
||||
dashboardResponse?.data,
|
||||
]);
|
||||
|
||||
if (isFetchingDashboardResponse) {
|
||||
return <Spinner tip="Loading.." />;
|
||||
}
|
||||
|
||||
if (dashboardResponse.isError) {
|
||||
if (isErrorDashboardResponse) {
|
||||
return (
|
||||
<Card>
|
||||
<Typography>{SOMETHING_WENT_WRONG}</Typography>
|
||||
@@ -54,16 +95,11 @@ function DashboardWidget(): JSX.Element | null {
|
||||
|
||||
return (
|
||||
<NewWidget
|
||||
yAxisUnit={selectedWidget?.yAxisUnit}
|
||||
selectedGraph={selectedGraph}
|
||||
fillSpans={selectedWidget?.fillSpans}
|
||||
dashboardId={dashboardId}
|
||||
selectedGraph={graphType}
|
||||
enableDrillDown={isDrilldownEnabled()}
|
||||
selectedDashboard={selectedDashboard}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface DashboardWidgetPageParams {
|
||||
dashboardId: string;
|
||||
}
|
||||
|
||||
export default DashboardWidget;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from 'mocks-server/__mockdata__/dashboards';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { fireEvent, render, waitFor } from 'tests/test-utils';
|
||||
|
||||
jest.mock('container/DashboardContainer/DashboardDescription/utils', () => ({
|
||||
@@ -19,11 +18,6 @@ jest.mock('container/DashboardContainer/DashboardDescription/utils', () => ({
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
useRouteMatch: jest.fn().mockReturnValue({
|
||||
params: {
|
||||
dashboardId: 4,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockWindowOpen = jest.fn();
|
||||
@@ -47,9 +41,7 @@ describe('dashboard list page', () => {
|
||||
<MemoryRouter
|
||||
initialEntries={['/dashbords?columnKey=asgard&order=stones&page=1']}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
<DashboardsList />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -71,9 +63,7 @@ describe('dashboard list page', () => {
|
||||
<MemoryRouter
|
||||
initialEntries={['/dashbords?columnKey=createdAt&order=descend&page=1']}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
<DashboardsList />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -92,9 +82,7 @@ describe('dashboard list page', () => {
|
||||
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||
]}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
<DashboardsList />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -135,9 +123,7 @@ describe('dashboard list page', () => {
|
||||
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||
]}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
<DashboardsList />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -164,9 +150,7 @@ describe('dashboard list page', () => {
|
||||
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||
]}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
<DashboardsList />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -196,9 +180,7 @@ describe('dashboard list page', () => {
|
||||
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||
]}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
<DashboardsList />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ function SettingsPage(): JSX.Element {
|
||||
isAdmin &&
|
||||
(item.key === ROUTES.BILLING ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.MEMBERS_SETTINGS ||
|
||||
item.key === ROUTES.MY_SETTINGS ||
|
||||
item.key === ROUTES.SHORTCUTS)
|
||||
),
|
||||
|
||||
@@ -36,6 +36,7 @@ export const getRoutes = (
|
||||
if (isWorkspaceBlocked && isAdmin) {
|
||||
settings.push(
|
||||
...organizationSettings(t),
|
||||
...membersSettings(t),
|
||||
...mySettings(t),
|
||||
...billingSettings(t),
|
||||
...keyboardShortcuts(t),
|
||||
|
||||
@@ -14,13 +14,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery, UseQueryResult } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { Modal } from 'antd';
|
||||
import getDashboard from 'api/v1/dashboards/id/get';
|
||||
import locked from 'api/v1/dashboards/id/lock';
|
||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||
@@ -83,14 +81,11 @@ export const DashboardContext = createContext<IDashboardContext>({
|
||||
setColumnWidths: () => {},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
dashboardId: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function DashboardProvider({
|
||||
children,
|
||||
}: PropsWithChildren): JSX.Element {
|
||||
dashboardId,
|
||||
}: PropsWithChildren<{ dashboardId: string }>): JSX.Element {
|
||||
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
|
||||
|
||||
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
|
||||
@@ -100,11 +95,6 @@ export function DashboardProvider({
|
||||
setDashboardQueryRangeCalled,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
const isDashboardPage = useRouteMatch<Props>({
|
||||
path: ROUTES.DASHBOARD,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
@@ -115,11 +105,6 @@ export function DashboardProvider({
|
||||
|
||||
const [onModal, Content] = Modal.useModal();
|
||||
|
||||
const isDashboardWidgetPage = useRouteMatch<Props>({
|
||||
path: ROUTES.DASHBOARD_WIDGET,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
const [layouts, setLayouts] = useState<Layout[]>([]);
|
||||
|
||||
const [panelMap, setPanelMap] = useState<
|
||||
@@ -128,11 +113,6 @@ export function DashboardProvider({
|
||||
|
||||
const { isLoggedIn } = useAppContext();
|
||||
|
||||
const dashboardId =
|
||||
(isDashboardPage
|
||||
? isDashboardPage.params.dashboardId
|
||||
: isDashboardWidgetPage?.params.dashboardId) || '';
|
||||
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<Dashboard>();
|
||||
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
|
||||
const savedDashboardId = useDashboardVariablesSelector((s) => s.dashboardId);
|
||||
@@ -267,12 +247,11 @@ export function DashboardProvider({
|
||||
const dashboardResponse = useQuery(
|
||||
[
|
||||
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
||||
isDashboardPage?.params,
|
||||
dashboardId,
|
||||
globalTime.isAutoRefreshDisabled,
|
||||
],
|
||||
{
|
||||
enabled: (!!isDashboardPage || !!isDashboardWidgetPage) && isLoggedIn,
|
||||
enabled: !!dashboardId && isLoggedIn,
|
||||
queryFn: async () => {
|
||||
setIsDashboardFetching(true);
|
||||
try {
|
||||
@@ -392,11 +371,7 @@ export function DashboardProvider({
|
||||
|
||||
useEffect(() => {
|
||||
// make the call on tab visibility only if the user is on dashboard / widget page
|
||||
if (
|
||||
isVisible &&
|
||||
updatedTimeRef.current &&
|
||||
(!!isDashboardPage || !!isDashboardWidgetPage)
|
||||
) {
|
||||
if (isVisible && updatedTimeRef.current && !!dashboardId) {
|
||||
dashboardResponse.refetch();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -2,11 +2,10 @@ import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||
import getDashboard from 'api/v1/dashboards/id/get';
|
||||
import { DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED } from 'constants/queryCacheTime';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DashboardProvider, useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
@@ -19,30 +18,28 @@ jest.mock('api/v1/dashboards/id/get');
|
||||
jest.mock('api/v1/dashboards/id/lock');
|
||||
const mockGetDashboard = jest.mocked(getDashboard);
|
||||
|
||||
// Mock useRouteMatch to simulate different route scenarios
|
||||
const mockUseRouteMatch = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useRouteMatch: (): any => mockUseRouteMatch(),
|
||||
}));
|
||||
|
||||
// Mock other dependencies
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock only the essential dependencies for Dashboard provider
|
||||
jest.mock('providers/App/App', () => ({
|
||||
useAppContext: (): any => ({
|
||||
useAppContext: (): {
|
||||
isLoggedIn: boolean;
|
||||
user: { email: string; role: string };
|
||||
} => ({
|
||||
isLoggedIn: true,
|
||||
user: { email: 'test@example.com', role: 'ADMIN' },
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/ErrorModalProvider', () => ({
|
||||
useErrorModal: (): any => ({ showErrorModal: jest.fn() }),
|
||||
useErrorModal: (): { showErrorModal: jest.Mock } => ({
|
||||
showErrorModal: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
@@ -60,11 +57,10 @@ jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
|
||||
function TestComponent(): JSX.Element {
|
||||
const { dashboardResponse, selectedDashboard } = useDashboard();
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const dashboardId = selectedDashboard?.id;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="dashboard-id">{dashboardId}</div>
|
||||
<div data-testid="dashboard-id">{selectedDashboard?.id}</div>
|
||||
<div data-testid="query-status">{dashboardResponse.status}</div>
|
||||
<div data-testid="is-loading">{dashboardResponse.isLoading.toString()}</div>
|
||||
<div data-testid="is-fetching">
|
||||
@@ -94,27 +90,15 @@ function createTestQueryClient(): QueryClient {
|
||||
|
||||
// Helper to render with dashboard provider
|
||||
function renderWithDashboardProvider(
|
||||
initialRoute = '/dashboard/test-dashboard-id',
|
||||
routeMatchParams?: { dashboardId: string } | null,
|
||||
): any {
|
||||
dashboardId = 'test-dashboard-id',
|
||||
): RenderResult {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
// Mock the route match
|
||||
mockUseRouteMatch.mockReturnValue(
|
||||
routeMatchParams
|
||||
? {
|
||||
path: ROUTES.DASHBOARD,
|
||||
url: `/dashboard/${routeMatchParams.dashboardId}`,
|
||||
isExact: true,
|
||||
params: routeMatchParams,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
const initialRoute = dashboardId ? `/dashboard/${dashboardId}` : '/dashboard';
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[initialRoute]}>
|
||||
<DashboardProvider>
|
||||
<DashboardProvider dashboardId={dashboardId}>
|
||||
<TestComponent />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>
|
||||
@@ -188,7 +172,7 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
describe('Query Key Behavior', () => {
|
||||
it('should include route params in query key when on dashboard page', async () => {
|
||||
const dashboardId = 'test-dashboard-id';
|
||||
renderWithDashboardProvider(`/dashboard/${dashboardId}`, { dashboardId });
|
||||
renderWithDashboardProvider(dashboardId);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: dashboardId });
|
||||
@@ -203,30 +187,17 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
const newDashboardId = 'new-dashboard-id';
|
||||
|
||||
// First render with initial dashboard ID
|
||||
const { rerender } = renderWithDashboardProvider(
|
||||
`/dashboard/${initialDashboardId}`,
|
||||
{
|
||||
dashboardId: initialDashboardId,
|
||||
},
|
||||
);
|
||||
const { rerender } = renderWithDashboardProvider(initialDashboardId);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: initialDashboardId });
|
||||
});
|
||||
|
||||
// Change route params to simulate navigation
|
||||
mockUseRouteMatch.mockReturnValue({
|
||||
path: ROUTES.DASHBOARD,
|
||||
url: `/dashboard/${newDashboardId}`,
|
||||
isExact: true,
|
||||
params: { dashboardId: newDashboardId },
|
||||
});
|
||||
|
||||
// Rerender with new route
|
||||
// Rerender with new dashboard ID prop
|
||||
rerender(
|
||||
<QueryClientProvider client={createTestQueryClient()}>
|
||||
<MemoryRouter initialEntries={[`/dashboard/${newDashboardId}`]}>
|
||||
<DashboardProvider>
|
||||
<DashboardProvider dashboardId={newDashboardId}>
|
||||
<TestComponent />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>
|
||||
@@ -241,50 +212,24 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not fetch when not on dashboard page', () => {
|
||||
// Mock no route match (not on dashboard page)
|
||||
mockUseRouteMatch.mockReturnValue(null);
|
||||
|
||||
renderWithDashboardProvider('/some-other-page', null);
|
||||
it('should not fetch when no dashboardId is provided', () => {
|
||||
renderWithDashboardProvider('');
|
||||
|
||||
// Should not call the API
|
||||
expect(mockGetDashboard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle undefined route params gracefully', async () => {
|
||||
// Mock route match with undefined params
|
||||
mockUseRouteMatch.mockReturnValue({
|
||||
path: ROUTES.DASHBOARD,
|
||||
url: '/dashboard/undefined',
|
||||
isExact: true,
|
||||
params: undefined,
|
||||
});
|
||||
|
||||
renderWithDashboardProvider('/dashboard/undefined');
|
||||
|
||||
// Should not call API when params are undefined
|
||||
expect(mockGetDashboard).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cache Behavior', () => {
|
||||
it('should create separate cache entries for different route params', async () => {
|
||||
it('should create separate cache entries for different dashboardIds', async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
const dashboardId1 = 'dashboard-1';
|
||||
const dashboardId2 = 'dashboard-2';
|
||||
|
||||
// First dashboard
|
||||
mockUseRouteMatch.mockReturnValue({
|
||||
path: ROUTES.DASHBOARD,
|
||||
url: `/dashboard/${dashboardId1}`,
|
||||
isExact: true,
|
||||
params: { dashboardId: dashboardId1 },
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[`/dashboard/${dashboardId1}`]}>
|
||||
<DashboardProvider>
|
||||
<DashboardProvider dashboardId={dashboardId1}>
|
||||
<TestComponent />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>
|
||||
@@ -295,18 +240,10 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: dashboardId1 });
|
||||
});
|
||||
|
||||
// Second dashboard
|
||||
mockUseRouteMatch.mockReturnValue({
|
||||
path: ROUTES.DASHBOARD,
|
||||
url: `/dashboard/${dashboardId2}`,
|
||||
isExact: true,
|
||||
params: { dashboardId: dashboardId2 },
|
||||
});
|
||||
|
||||
rerender(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[`/dashboard/${dashboardId2}`]}>
|
||||
<DashboardProvider>
|
||||
<DashboardProvider dashboardId={dashboardId2}>
|
||||
<TestComponent />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>
|
||||
@@ -325,13 +262,11 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
expect(cacheKeys).toHaveLength(2);
|
||||
expect(cacheKeys[0]).toEqual([
|
||||
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
||||
{ dashboardId: dashboardId1 },
|
||||
dashboardId1,
|
||||
true, // globalTime.isAutoRefreshDisabled
|
||||
]);
|
||||
expect(cacheKeys[1]).toEqual([
|
||||
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
||||
{ dashboardId: dashboardId2 },
|
||||
dashboardId2,
|
||||
true, // globalTime.isAutoRefreshDisabled
|
||||
]);
|
||||
@@ -348,17 +283,10 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
const dashboardId = 'auto-refresh-dashboard';
|
||||
|
||||
mockUseRouteMatch.mockReturnValue({
|
||||
path: ROUTES.DASHBOARD,
|
||||
url: `/dashboard/${dashboardId}`,
|
||||
isExact: true,
|
||||
params: { dashboardId },
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[`/dashboard/${dashboardId}`]}>
|
||||
<DashboardProvider>
|
||||
<DashboardProvider dashboardId={dashboardId}>
|
||||
<TestComponent />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>
|
||||
@@ -375,7 +303,7 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
.find(
|
||||
(query) =>
|
||||
query.queryKey[0] === REACT_QUERY_KEY.DASHBOARD_BY_ID &&
|
||||
query.queryKey[3] === false,
|
||||
query.queryKey[2] === false,
|
||||
);
|
||||
expect(dashboardQuery).toBeDefined();
|
||||
expect((dashboardQuery as { cacheTime: number }).cacheTime).toBe(
|
||||
@@ -437,9 +365,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
// Empty URL variables - tests initialization flow
|
||||
mockGetUrlVariables.mockReturnValue({});
|
||||
|
||||
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
|
||||
dashboardId: DASHBOARD_ID,
|
||||
});
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -493,9 +419,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
.mockReturnValueOnce('development')
|
||||
.mockReturnValueOnce(['db', 'cache']);
|
||||
|
||||
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
|
||||
dashboardId: DASHBOARD_ID,
|
||||
});
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -555,9 +479,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
|
||||
mockGetUrlVariables.mockReturnValue(urlVariables);
|
||||
|
||||
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
|
||||
dashboardId: DASHBOARD_ID,
|
||||
});
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -593,9 +515,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
.mockReturnValueOnce('development')
|
||||
.mockReturnValueOnce(['api']);
|
||||
|
||||
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
|
||||
dashboardId: DASHBOARD_ID,
|
||||
});
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify normalization was called with the specific values and variable configs
|
||||
@@ -662,9 +582,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
} as any);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
|
||||
dashboardId: DASHBOARD_ID,
|
||||
});
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -706,9 +624,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
} as any);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
|
||||
dashboardId: DASHBOARD_ID,
|
||||
});
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -751,9 +667,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
} as any);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
|
||||
dashboardId: DASHBOARD_ID,
|
||||
});
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -795,9 +709,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
} as any);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
renderWithDashboardProvider(`/dashboard/${DASHBOARD_ID}`, {
|
||||
dashboardId: DASHBOARD_ID,
|
||||
});
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
|
||||
9
frontend/src/utils/maskedKey.ts
Normal file
9
frontend/src/utils/maskedKey.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Masks a key string, showing only the first 2 and last 2 characters.
|
||||
*/
|
||||
export function getMaskedKey(key: string): string {
|
||||
if (!key || key.length < 4) {
|
||||
return key || 'N/A';
|
||||
}
|
||||
return `${key.substring(0, 2)}·······${key.slice(-2).trim()}`;
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (service *Service) SyncServers(ctx context.Context) error {
|
||||
|
||||
service.serversMtx.Lock()
|
||||
for _, org := range orgs {
|
||||
config, err := service.getConfig(ctx, org.ID.StringValue())
|
||||
config, _, err := service.getConfig(ctx, org.ID.StringValue())
|
||||
if err != nil {
|
||||
service.settings.Logger().ErrorContext(ctx, "failed to get alertmanager config for org", "org_id", org.ID.StringValue(), "error", err)
|
||||
continue
|
||||
@@ -171,7 +171,7 @@ func (service *Service) Stop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (service *Service) newServer(ctx context.Context, orgID string) (*alertmanagerserver.Server, error) {
|
||||
config, err := service.getConfig(ctx, orgID)
|
||||
config, storedHash, err := service.getConfig(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -181,13 +181,16 @@ func (service *Service) newServer(ctx context.Context, orgID string) (*alertmana
|
||||
return nil, err
|
||||
}
|
||||
|
||||
beforeCompareAndSelectHash := config.StoreableConfig().Hash
|
||||
config, err = service.compareAndSelectConfig(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if beforeCompareAndSelectHash == config.StoreableConfig().Hash {
|
||||
// compare against the hash of the config stored in the DB (before overlays
|
||||
// were applied by getConfig). This ensures that overlay changes (e.g. new
|
||||
// defaults from an upstream upgrade or something similar) trigger a DB update
|
||||
// so that other code paths reading directly from the store see the up-to-date config.
|
||||
if storedHash == config.StoreableConfig().Hash {
|
||||
service.settings.Logger().DebugContext(ctx, "skipping config store update for org", "org_id", orgID, "hash", config.StoreableConfig().Hash)
|
||||
return server, nil
|
||||
}
|
||||
@@ -200,27 +203,33 @@ func (service *Service) newServer(ctx context.Context, orgID string) (*alertmana
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, error) {
|
||||
// getConfig returns the config for the given orgID with overlays applied, along
|
||||
// with the hash that was stored in the DB before overlays. When no config exists
|
||||
// in the store yet the stored hash is empty.
|
||||
func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmanagertypes.Config, string, error) {
|
||||
config, err := service.configStore.Get(ctx, orgID)
|
||||
var storedHash string
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
config, err = alertmanagertypes.NewDefaultConfig(service.config.Global, service.config.Route, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
} else {
|
||||
storedHash = config.StoreableConfig().Hash
|
||||
}
|
||||
|
||||
if err := config.SetGlobalConfig(service.config.Global); err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
if err := config.SetRouteConfig(service.config.Route); err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
return config, storedHash, nil
|
||||
}
|
||||
|
||||
func (service *Service) compareAndSelectConfig(ctx context.Context, incomingConfig *alertmanagertypes.Config) (*alertmanagertypes.Config, error) {
|
||||
|
||||
@@ -2,9 +2,11 @@ module base
|
||||
|
||||
type user
|
||||
|
||||
type serviceaccount
|
||||
|
||||
type role
|
||||
relations
|
||||
define assignee: [user]
|
||||
define assignee: [user, serviceaccount]
|
||||
|
||||
type organisation
|
||||
relations
|
||||
|
||||
77
pkg/modules/cloudintegration/cloudintegration.go
Normal file
77
pkg/modules/cloudintegration/cloudintegration.go
Normal file
@@ -0,0 +1,77 @@
|
||||
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 {
|
||||
Type() cloudintegrationtypes.CloudProviderType
|
||||
|
||||
// GenerateConnectionArtifact generates cloud provider specific connection information,
|
||||
// client side handles how this information is shown
|
||||
GenerateConnectionArtifact(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
provider cloudintegrationtypes.CloudProviderType,
|
||||
request *cloudintegrationtypes.PostableConnectionArtifact,
|
||||
) (*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.ConnectedAccountsList, error)
|
||||
|
||||
// DisconnectAccount soft deletes/removes a cloud integration account.
|
||||
DisconnectAccount(ctx context.Context, orgID, accountID string) error
|
||||
|
||||
// UpdateAccountConfig updates cloud integration account config
|
||||
UpdateAccountConfig(ctx context.Context, orgId, accountId valuer.UUID, config []byte) (cloudintegrationtypes.Account, error)
|
||||
|
||||
// ListServices return list of services for a cloud provider attached with the accountID. This just returns a summary
|
||||
ListServices(ctx context.Context, orgID valuer.UUID, accountID *valuer.UUID) (*cloudintegrationtypes.ServiceSummaries, error)
|
||||
|
||||
// GetServiceDetails returns service definition details for a serviceId. This returns config and
|
||||
// other details required to show in service details page on client.
|
||||
GetServiceDetails(ctx context.Context, orgID valuer.UUID, accountID *valuer.UUID) (*cloudintegrationtypes.Service, error)
|
||||
|
||||
// UpdateServiceConfig updates cloud integration service config
|
||||
UpdateServiceConfig(ctx context.Context, serviceId string, orgID valuer.UUID, config []byte) (*cloudintegrationtypes.ServiceSummary, 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)
|
||||
|
||||
// GetDashboard returns dashboard json for a give cloud integration service dashboard.
|
||||
// this only returns the dashboard when account is connected and service is enabled
|
||||
GetDashboard(ctx context.Context, id string, orgID valuer.UUID) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
// GetAvailableDashboards returns list of available dashboards across all connected cloud integration accounts in the org.
|
||||
// this list gets added to dashboard list page
|
||||
GetAvailableDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
AgentCheckIn(http.ResponseWriter, *http.Request)
|
||||
|
||||
GenerateConnectionParams(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)
|
||||
}
|
||||
@@ -111,7 +111,12 @@ func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
serviceAccount.Update(req.Name, req.Email, req.Roles)
|
||||
err = serviceAccount.Update(req.Name, req.Email, req.Roles)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.module.Update(ctx, valuer.MustNewUUID(claims.OrgID), serviceAccount)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
@@ -147,7 +152,12 @@ func (handler *handler) UpdateStatus(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
serviceAccount.UpdateStatus(req.Status)
|
||||
err = serviceAccount.UpdateStatus(req.Status)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.module.UpdateStatus(ctx, valuer.MustNewUUID(claims.OrgID), serviceAccount)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
@@ -290,7 +300,7 @@ func (handler *handler) UpdateFactorAPIKey(rw http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
factorAPIKey.Update(req.Name, req.ExpiresAt)
|
||||
err = handler.module.UpdateFactorAPIKey(ctx, serviceAccount.ID, factorAPIKey)
|
||||
err = handler.module.UpdateFactorAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), serviceAccount.ID, factorAPIKey)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/emailing"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
@@ -33,7 +34,7 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, serviceAcco
|
||||
}
|
||||
|
||||
// authz actions cannot run in sql transactions
|
||||
err = module.authz.Grant(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil))
|
||||
err = module.authz.Grant(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, serviceAccount.ID.String(), orgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,6 +61,24 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, serviceAcco
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) GetOrCreate(ctx context.Context, serviceAccount *serviceaccounttypes.ServiceAccount) (*serviceaccounttypes.ServiceAccount, error) {
|
||||
existingServiceAccount, err := module.store.GetActiveByOrgIDAndName(ctx, serviceAccount.OrgID, serviceAccount.Name)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existingServiceAccount != nil {
|
||||
return serviceAccount, nil
|
||||
}
|
||||
|
||||
err = module.Create(ctx, serviceAccount.OrgID, serviceAccount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serviceAccount, nil
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*serviceaccounttypes.ServiceAccount, error) {
|
||||
storableServiceAccount, err := module.store.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
@@ -138,7 +157,7 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, input *serv
|
||||
|
||||
// gets the role diff if any to modify grants.
|
||||
grants, revokes := serviceAccount.PatchRoles(input)
|
||||
err = module.authz.ModifyGrant(ctx, orgID, revokes, grants, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil))
|
||||
err = module.authz.ModifyGrant(ctx, orgID, revokes, grants, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, serviceAccount.ID.String(), orgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -171,26 +190,28 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, input *serv
|
||||
}
|
||||
|
||||
func (module *module) UpdateStatus(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error {
|
||||
serviceAccount, err := module.Get(ctx, orgID, input.ID)
|
||||
err := module.authz.Revoke(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, input.ID.String(), orgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if input.Status == serviceAccount.Status {
|
||||
return nil
|
||||
}
|
||||
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
// revoke all the API keys on disable
|
||||
err := module.store.RevokeAllFactorAPIKeys(ctx, input.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch input.Status {
|
||||
case serviceaccounttypes.StatusActive:
|
||||
err := module.activateServiceAccount(ctx, orgID, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case serviceaccounttypes.StatusDisabled:
|
||||
err := module.disableServiceAccount(ctx, orgID, input)
|
||||
// update the status but do not delete the role mappings as we will use them for audits
|
||||
err = module.store.Update(ctx, orgID, serviceaccounttypes.NewStorableServiceAccount(input))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -203,7 +224,7 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
}
|
||||
|
||||
// revoke from authz first as this cannot run in sql transaction
|
||||
err = module.authz.Revoke(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil))
|
||||
err = module.authz.Revoke(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, serviceAccount.ID.String(), orgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -276,8 +297,13 @@ func (module *module) ListFactorAPIKey(ctx context.Context, serviceAccountID val
|
||||
return serviceaccounttypes.NewFactorAPIKeyFromStorables(storables), nil
|
||||
}
|
||||
|
||||
func (module *module) UpdateFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, factorAPIKey *serviceaccounttypes.FactorAPIKey) error {
|
||||
return module.store.UpdateFactorAPIKey(ctx, serviceAccountID, serviceaccounttypes.NewStorableFactorAPIKey(factorAPIKey))
|
||||
func (module *module) UpdateFactorAPIKey(ctx context.Context, _ valuer.UUID, serviceAccountID valuer.UUID, factorAPIKey *serviceaccounttypes.FactorAPIKey) error {
|
||||
err := module.store.UpdateFactorAPIKey(ctx, serviceAccountID, serviceaccounttypes.NewStorableFactorAPIKey(factorAPIKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) RevokeFactorAPIKey(ctx context.Context, serviceAccountID valuer.UUID, id valuer.UUID) error {
|
||||
@@ -307,45 +333,3 @@ func (module *module) RevokeFactorAPIKey(ctx context.Context, serviceAccountID v
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) disableServiceAccount(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error {
|
||||
err := module.authz.Revoke(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.String(), orgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
// revoke all the API keys on disable
|
||||
err := module.store.RevokeAllFactorAPIKeys(ctx, input.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update the status but do not delete the role mappings as we will reuse them on activation.
|
||||
err = module.Update(ctx, orgID, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) activateServiceAccount(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error {
|
||||
err := module.authz.Grant(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.String(), orgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.Update(ctx, orgID, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -48,6 +48,25 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
|
||||
return storable, nil
|
||||
}
|
||||
|
||||
func (store *store) GetActiveByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*serviceaccounttypes.StorableServiceAccount, error) {
|
||||
storable := new(serviceaccounttypes.StorableServiceAccount)
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(storable).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("name = ?", name).
|
||||
Where("status = ?", serviceaccounttypes.StatusActive).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeServiceAccountNotFound, "service account with name: %s doesn't exist in org: %s", name, orgID.String())
|
||||
}
|
||||
|
||||
return storable, nil
|
||||
}
|
||||
|
||||
func (store *store) GetByID(ctx context.Context, id valuer.UUID) (*serviceaccounttypes.StorableServiceAccount, error) {
|
||||
storable := new(serviceaccounttypes.StorableServiceAccount)
|
||||
|
||||
@@ -188,7 +207,7 @@ func (store *store) CreateFactorAPIKey(ctx context.Context, storable *serviceacc
|
||||
Model(storable).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, serviceaccounttypes.ErrCodeServiceAccountFactorAPIKeyAlreadyExists, "api key with name: %s already exists for service account: %s", storable.Name, storable.ServiceAccountID)
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, serviceaccounttypes.ErrCodeAPIKeyAlreadyExists, "api key with name: %s already exists for service account: %s", storable.Name, storable.ServiceAccountID)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -206,7 +225,7 @@ func (store *store) GetFactorAPIKey(ctx context.Context, serviceAccountID valuer
|
||||
Where("service_account_id = ?", serviceAccountID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeServiceAccounFactorAPIKeytNotFound, "api key with id: %s doesn't exist for service account: %s", id, serviceAccountID)
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, serviceaccounttypes.ErrCodeAPIKeytNotFound, "api key with id: %s doesn't exist for service account: %s", id, serviceAccountID)
|
||||
}
|
||||
|
||||
return storable, nil
|
||||
|
||||
@@ -15,6 +15,9 @@ type Module interface {
|
||||
// Gets a service account by id.
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*serviceaccounttypes.ServiceAccount, error)
|
||||
|
||||
// Gets or creates a service account by name
|
||||
GetOrCreate(context.Context, *serviceaccounttypes.ServiceAccount) (*serviceaccounttypes.ServiceAccount, error)
|
||||
|
||||
// Gets a service account by id without fetching roles.
|
||||
GetWithoutRoles(context.Context, valuer.UUID, valuer.UUID) (*serviceaccounttypes.ServiceAccount, error)
|
||||
|
||||
@@ -40,7 +43,7 @@ type Module interface {
|
||||
ListFactorAPIKey(context.Context, valuer.UUID) ([]*serviceaccounttypes.FactorAPIKey, error)
|
||||
|
||||
// Updates an existing API key for a service account
|
||||
UpdateFactorAPIKey(context.Context, valuer.UUID, *serviceaccounttypes.FactorAPIKey) error
|
||||
UpdateFactorAPIKey(context.Context, valuer.UUID, valuer.UUID, *serviceaccounttypes.FactorAPIKey) error
|
||||
|
||||
// Revokes an existing API key for a service account
|
||||
RevokeFactorAPIKey(context.Context, valuer.UUID, valuer.UUID) error
|
||||
|
||||
@@ -20,19 +20,20 @@ var (
|
||||
)
|
||||
|
||||
var TypeableRelations = map[Type][]Relation{
|
||||
TypeUser: {RelationRead, RelationUpdate, RelationDelete},
|
||||
TypeRole: {RelationAssignee, RelationRead, RelationUpdate, RelationDelete},
|
||||
TypeOrganization: {RelationRead, RelationUpdate, RelationDelete},
|
||||
TypeMetaResource: {RelationRead, RelationUpdate, RelationDelete},
|
||||
TypeMetaResources: {RelationCreate, RelationList},
|
||||
TypeUser: {RelationRead, RelationUpdate, RelationDelete},
|
||||
TypeServiceAccount: {RelationRead, RelationUpdate, RelationDelete},
|
||||
TypeRole: {RelationAssignee, RelationRead, RelationUpdate, RelationDelete},
|
||||
TypeOrganization: {RelationRead, RelationUpdate, RelationDelete},
|
||||
TypeMetaResource: {RelationRead, RelationUpdate, RelationDelete},
|
||||
TypeMetaResources: {RelationCreate, RelationList},
|
||||
}
|
||||
|
||||
var RelationsTypeable = map[Relation][]Type{
|
||||
RelationCreate: {TypeMetaResources},
|
||||
RelationRead: {TypeUser, TypeRole, TypeOrganization, TypeMetaResource},
|
||||
RelationRead: {TypeUser, TypeServiceAccount, TypeRole, TypeOrganization, TypeMetaResource},
|
||||
RelationList: {TypeMetaResources},
|
||||
RelationUpdate: {TypeUser, TypeRole, TypeOrganization, TypeMetaResource},
|
||||
RelationDelete: {TypeUser, TypeRole, TypeOrganization, TypeMetaResource},
|
||||
RelationUpdate: {TypeUser, TypeServiceAccount, TypeRole, TypeOrganization, TypeMetaResource},
|
||||
RelationDelete: {TypeUser, TypeServiceAccount, TypeRole, TypeOrganization, TypeMetaResource},
|
||||
RelationAssignee: {TypeRole},
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,12 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
typeUserSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
|
||||
typeRoleSelectorRegex = regexp.MustCompile(`^([a-z-]{1,50}|\*)$`)
|
||||
typeAnonymousSelectorRegex = regexp.MustCompile(`^\*$`)
|
||||
typeOrganizationSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
|
||||
typeMetaResourceSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
|
||||
typeUserSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
|
||||
typeServiceAccountSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
|
||||
typeRoleSelectorRegex = regexp.MustCompile(`^([a-z-]{1,50}|\*)$`)
|
||||
typeAnonymousSelectorRegex = regexp.MustCompile(`^\*$`)
|
||||
typeOrganizationSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
|
||||
typeMetaResourceSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`)
|
||||
// metaresources selectors are used to select either all or none until we introduce some hierarchy here.
|
||||
typeMetaResourcesSelectorRegex = regexp.MustCompile(`^\*$`)
|
||||
)
|
||||
@@ -98,6 +99,11 @@ func IsValidSelector(typed Type, selector string) error {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeUserSelectorRegex.String())
|
||||
}
|
||||
return nil
|
||||
case TypeServiceAccount:
|
||||
if !typeServiceAccountSelectorRegex.MatchString(selector) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeServiceAccountSelectorRegex.String())
|
||||
}
|
||||
return nil
|
||||
case TypeRole:
|
||||
if !typeRoleSelectorRegex.MatchString(selector) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeRoleSelectorRegex.String())
|
||||
|
||||
@@ -15,19 +15,21 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
TypeUser = Type{valuer.NewString("user")}
|
||||
TypeAnonymous = Type{valuer.NewString("anonymous")}
|
||||
TypeRole = Type{valuer.NewString("role")}
|
||||
TypeOrganization = Type{valuer.NewString("organization")}
|
||||
TypeMetaResource = Type{valuer.NewString("metaresource")}
|
||||
TypeMetaResources = Type{valuer.NewString("metaresources")}
|
||||
TypeUser = Type{valuer.NewString("user")}
|
||||
TypeServiceAccount = Type{valuer.NewString("serviceaccount")}
|
||||
TypeAnonymous = Type{valuer.NewString("anonymous")}
|
||||
TypeRole = Type{valuer.NewString("role")}
|
||||
TypeOrganization = Type{valuer.NewString("organization")}
|
||||
TypeMetaResource = Type{valuer.NewString("metaresource")}
|
||||
TypeMetaResources = Type{valuer.NewString("metaresources")}
|
||||
)
|
||||
|
||||
var (
|
||||
TypeableUser = &typeableUser{}
|
||||
TypeableAnonymous = &typeableAnonymous{}
|
||||
TypeableRole = &typeableRole{}
|
||||
TypeableOrganization = &typeableOrganization{}
|
||||
TypeableUser = &typeableUser{}
|
||||
TypeableServiceAccount = &typeableServiceAccount{}
|
||||
TypeableAnonymous = &typeableAnonymous{}
|
||||
TypeableRole = &typeableRole{}
|
||||
TypeableOrganization = &typeableOrganization{}
|
||||
)
|
||||
|
||||
type Typeable interface {
|
||||
@@ -53,6 +55,8 @@ func NewType(input string) (Type, error) {
|
||||
switch input {
|
||||
case "user":
|
||||
return TypeUser, nil
|
||||
case "serviceaccount":
|
||||
return TypeServiceAccount, nil
|
||||
case "role":
|
||||
return TypeRole, nil
|
||||
case "organization":
|
||||
@@ -88,6 +92,8 @@ func NewTypeableFromType(typed Type, name Name) (Typeable, error) {
|
||||
return TypeableRole, nil
|
||||
case TypeUser:
|
||||
return TypeableUser, nil
|
||||
case TypeServiceAccount:
|
||||
return TypeableServiceAccount, nil
|
||||
case TypeOrganization:
|
||||
return TypeableOrganization, nil
|
||||
case TypeMetaResource:
|
||||
|
||||
38
pkg/types/authtypes/typeable_serviceaccount.go
Normal file
38
pkg/types/authtypes/typeable_serviceaccount.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package authtypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
)
|
||||
|
||||
var _ Typeable = new(typeableServiceAccount)
|
||||
|
||||
type typeableServiceAccount struct{}
|
||||
|
||||
func (typeableServiceAccount *typeableServiceAccount) Tuples(subject string, relation Relation, selectors []Selector, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
|
||||
for _, selector := range selectors {
|
||||
object := typeableServiceAccount.Prefix(orgID) + "/" + selector.String()
|
||||
tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object})
|
||||
}
|
||||
|
||||
return tuples, nil
|
||||
}
|
||||
|
||||
func (typeableServiceAccount *typeableServiceAccount) Type() Type {
|
||||
return TypeServiceAccount
|
||||
}
|
||||
|
||||
func (typeableServiceAccount *typeableServiceAccount) Name() Name {
|
||||
return MustNewName("serviceaccount")
|
||||
}
|
||||
|
||||
// example: serviceaccount:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/serviceaccount
|
||||
func (typeableServiceAccount *typeableServiceAccount) Prefix(orgID valuer.UUID) string {
|
||||
return typeableServiceAccount.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableServiceAccount.Name().String()
|
||||
}
|
||||
|
||||
func (typeableServiceAccount *typeableServiceAccount) Scope(relation Relation) string {
|
||||
return typeableServiceAccount.Name().String() + ":" + relation.StringValue()
|
||||
}
|
||||
42
pkg/types/cloudintegrationtypes/account.go
Normal file
42
pkg/types/cloudintegrationtypes/account.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type (
|
||||
ConnectedAccountsList struct {
|
||||
Accounts []*Account `json:"accounts"`
|
||||
}
|
||||
|
||||
GettableConnectedAccountsList = ConnectedAccountsList
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
108
pkg/types/cloudintegrationtypes/cloudintegration.go
Normal file
108
pkg/types/cloudintegrationtypes/cloudintegration.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// AgentReport 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_integrations(id),on_delete:cascade"`
|
||||
}
|
||||
|
||||
type CloudIntegrationStore interface {
|
||||
// GetAccountByID returns a cloud integration account by id
|
||||
GetAccountByID(ctx context.Context, id, orgID valuer.UUID, provider CloudProviderType) (*StorableCloudIntegration, error)
|
||||
|
||||
// UpsertAccount creates or updates a cloud integration account
|
||||
UpsertAccount(ctx context.Context, account *StorableCloudIntegration) error
|
||||
|
||||
// RemoveAccount marks a cloud integration account as removed by setting the RemovedAt field
|
||||
RemoveAccount(ctx context.Context, id, orgID valuer.UUID, provider CloudProviderType) error
|
||||
|
||||
// ListAllConnectedAccounts returns all the cloud integration accounts for the org and cloud provider
|
||||
GetAllConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider CloudProviderType) ([]*StorableCloudIntegration, error)
|
||||
|
||||
// Get connected integration account 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, serviceType string, cloudIntegrationID valuer.UUID) (*StorableCloudIntegrationService, error)
|
||||
|
||||
// UpsertService creates or updates a cloud integration service for the given cloud integration id and service type
|
||||
UpsertService(ctx context.Context, service *StorableCloudIntegrationService) error
|
||||
|
||||
// GetAllServicesForIntegration returns all the cloud integration services for the given cloud integration id
|
||||
GetAllServicesForIntegration(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*StorableCloudIntegrationService, error)
|
||||
}
|
||||
|
||||
// For serializing 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)
|
||||
}
|
||||
|
||||
// For serializing to 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
|
||||
}
|
||||
41
pkg/types/cloudintegrationtypes/cloudprovider.go
Normal file
41
pkg/types/cloudintegrationtypes/cloudprovider.go
Normal file
@@ -0,0 +1,41 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
84
pkg/types/cloudintegrationtypes/connection.go
Normal file
84
pkg/types/cloudintegrationtypes/connection.go
Normal file
@@ -0,0 +1,84 @@
|
||||
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 (
|
||||
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 fields for backward compatibility
|
||||
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 = AgentCheckInRequest
|
||||
|
||||
GettableAgentCheckInResponse = AgentCheckInResponse
|
||||
|
||||
AgentCheckInResponse struct {
|
||||
// Older fields for backward compatibility
|
||||
CloudIntegrationId string `json:"cloudIntegrationId"`
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
// 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
|
||||
IntegrationConfig *IntegrationConfig `json:"integrationConfig,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"`
|
||||
}
|
||||
)
|
||||
103
pkg/types/cloudintegrationtypes/regions.go
Normal file
103
pkg/types/cloudintegrationtypes/regions.go
Normal file
@@ -0,0 +1,103 @@
|
||||
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]bool{
|
||||
"af-south-1": true, // Africa (Cape Town).
|
||||
"ap-east-1": true, // Asia Pacific (Hong Kong).
|
||||
"ap-northeast-1": true, // Asia Pacific (Tokyo).
|
||||
"ap-northeast-2": true, // Asia Pacific (Seoul).
|
||||
"ap-northeast-3": true, // Asia Pacific (Osaka).
|
||||
"ap-south-1": true, // Asia Pacific (Mumbai).
|
||||
"ap-south-2": true, // Asia Pacific (Hyderabad).
|
||||
"ap-southeast-1": true, // Asia Pacific (Singapore).
|
||||
"ap-southeast-2": true, // Asia Pacific (Sydney).
|
||||
"ap-southeast-3": true, // Asia Pacific (Jakarta).
|
||||
"ap-southeast-4": true, // Asia Pacific (Melbourne).
|
||||
"ca-central-1": true, // Canada (Central).
|
||||
"ca-west-1": true, // Canada West (Calgary).
|
||||
"eu-central-1": true, // Europe (Frankfurt).
|
||||
"eu-central-2": true, // Europe (Zurich).
|
||||
"eu-north-1": true, // Europe (Stockholm).
|
||||
"eu-south-1": true, // Europe (Milan).
|
||||
"eu-south-2": true, // Europe (Spain).
|
||||
"eu-west-1": true, // Europe (Ireland).
|
||||
"eu-west-2": true, // Europe (London).
|
||||
"eu-west-3": true, // Europe (Paris).
|
||||
"il-central-1": true, // Israel (Tel Aviv).
|
||||
"me-central-1": true, // Middle East (UAE).
|
||||
"me-south-1": true, // Middle East (Bahrain).
|
||||
"sa-east-1": true, // South America (Sao Paulo).
|
||||
"us-east-1": true, // US East (N. Virginia).
|
||||
"us-east-2": true, // US East (Ohio).
|
||||
"us-west-1": true, // US West (N. California).
|
||||
"us-west-2": true, // US West (Oregon).
|
||||
}
|
||||
|
||||
// List of all valid cloud regions for Microsoft Azure
|
||||
var ValidAzureRegions = map[string]bool{
|
||||
"australiacentral": true, // Australia Central
|
||||
"australiacentral2": true, // Australia Central 2
|
||||
"australiaeast": true, // Australia East
|
||||
"australiasoutheast": true, // Australia Southeast
|
||||
"austriaeast": true, // Austria East
|
||||
"belgiumcentral": true, // Belgium Central
|
||||
"brazilsouth": true, // Brazil South
|
||||
"brazilsoutheast": true, // Brazil Southeast
|
||||
"canadacentral": true, // Canada Central
|
||||
"canadaeast": true, // Canada East
|
||||
"centralindia": true, // Central India
|
||||
"centralus": true, // Central US
|
||||
"chilecentral": true, // Chile Central
|
||||
"denmarkeast": true, // Denmark East
|
||||
"eastasia": true, // East Asia
|
||||
"eastus": true, // East US
|
||||
"eastus2": true, // East US 2
|
||||
"francecentral": true, // France Central
|
||||
"francesouth": true, // France South
|
||||
"germanynorth": true, // Germany North
|
||||
"germanywestcentral": true, // Germany West Central
|
||||
"indonesiacentral": true, // Indonesia Central
|
||||
"israelcentral": true, // Israel Central
|
||||
"italynorth": true, // Italy North
|
||||
"japaneast": true, // Japan East
|
||||
"japanwest": true, // Japan West
|
||||
"koreacentral": true, // Korea Central
|
||||
"koreasouth": true, // Korea South
|
||||
"malaysiawest": true, // Malaysia West
|
||||
"mexicocentral": true, // Mexico Central
|
||||
"newzealandnorth": true, // New Zealand North
|
||||
"northcentralus": true, // North Central US
|
||||
"northeurope": true, // North Europe
|
||||
"norwayeast": true, // Norway East
|
||||
"norwaywest": true, // Norway West
|
||||
"polandcentral": true, // Poland Central
|
||||
"qatarcentral": true, // Qatar Central
|
||||
"southafricanorth": true, // South Africa North
|
||||
"southafricawest": true, // South Africa West
|
||||
"southcentralus": true, // South Central US
|
||||
"southindia": true, // South India
|
||||
"southeastasia": true, // Southeast Asia
|
||||
"spaincentral": true, // Spain Central
|
||||
"swedencentral": true, // Sweden Central
|
||||
"switzerlandnorth": true, // Switzerland North
|
||||
"switzerlandwest": true, // Switzerland West
|
||||
"uaecentral": true, // UAE Central
|
||||
"uaenorth": true, // UAE North
|
||||
"uksouth": true, // UK South
|
||||
"ukwest": true, // UK West
|
||||
"westcentralus": true, // West Central US
|
||||
"westeurope": true, // West Europe
|
||||
"westindia": true, // West India
|
||||
"westus": true, // West US
|
||||
"westus2": true, // West US 2
|
||||
"westus3": true, // West US 3
|
||||
}
|
||||
190
pkg/types/cloudintegrationtypes/service.go
Normal file
190
pkg/types/cloudintegrationtypes/service.go
Normal file
@@ -0,0 +1,190 @@
|
||||
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 (
|
||||
ServiceSummaries struct {
|
||||
Services []*ServiceSummary `json:"services"`
|
||||
}
|
||||
|
||||
ServiceSummary struct {
|
||||
ServiceDefinitionMetadata
|
||||
ServiceConfig *ServiceConfig `json:"service_config"`
|
||||
}
|
||||
|
||||
GettableServiceSummary = ServiceSummary
|
||||
|
||||
GettableServiceSummaries = ServiceSummaries
|
||||
|
||||
Service struct {
|
||||
ServiceDefinition
|
||||
ServiceConfig *ServiceConfig `json:"service_config"`
|
||||
}
|
||||
|
||||
GettableService = Service
|
||||
)
|
||||
|
||||
type ServiceConfig struct {
|
||||
AWS *AWSServiceConfig `json:"aws,omitempty"`
|
||||
}
|
||||
|
||||
type AWSServiceConfig struct {
|
||||
Logs *AWSServiceLogsConfig `json:"logs"`
|
||||
Metrics *AWSServiceMetricsConfig `json:"metrics"`
|
||||
}
|
||||
|
||||
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:"data_collected"`
|
||||
Strategy *CollectionStrategy `json:"telemetry_collection_strategy"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
}
|
||||
@@ -11,20 +11,22 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeServiceAccountFactorAPIkeyInvalidInput = errors.MustNewCode("service_account_factor_api_key_invalid_input")
|
||||
ErrCodeServiceAccountFactorAPIKeyAlreadyExists = errors.MustNewCode("service_account_factor_api_key_already_exists")
|
||||
ErrCodeServiceAccounFactorAPIKeytNotFound = errors.MustNewCode("service_account_factor_api_key_not_found")
|
||||
ErrCodeAPIkeyInvalidInput = errors.MustNewCode("service_account_factor_api_key_invalid_input")
|
||||
ErrCodeAPIKeyAlreadyExists = errors.MustNewCode("service_account_factor_api_key_already_exists")
|
||||
ErrCodeAPIKeytNotFound = errors.MustNewCode("service_account_factor_api_key_not_found")
|
||||
ErrCodeAPIKeyExpired = errors.MustNewCode("api_key_expired")
|
||||
ErrCodeAPIkeyOlderLastObservedAt = errors.MustNewCode("api_key_older_last_observed_at")
|
||||
)
|
||||
|
||||
type StorableFactorAPIKey struct {
|
||||
bun.BaseModel `bun:"table:factor_api_key"`
|
||||
bun.BaseModel `bun:"table:factor_api_key,alias:factor_api_key"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Name string `bun:"name"`
|
||||
Key string `bun:"key"`
|
||||
ExpiresAt uint64 `bun:"expires_at"`
|
||||
LastUsed time.Time `bun:"last_used"`
|
||||
LastObservedAt time.Time `bun:"last_observed_at"`
|
||||
ServiceAccountID string `bun:"service_account_id"`
|
||||
}
|
||||
|
||||
@@ -33,9 +35,9 @@ type FactorAPIKey struct {
|
||||
types.TimeAuditable
|
||||
Name string `json:"name" requrired:"true"`
|
||||
Key string `json:"key" required:"true"`
|
||||
ExpiresAt uint64 `json:"expires_at" required:"true"`
|
||||
LastUsed time.Time `json:"last_used" required:"true"`
|
||||
ServiceAccountID valuer.UUID `json:"service_account_id" required:"true"`
|
||||
ExpiresAt uint64 `json:"expiresAt" required:"true"`
|
||||
LastObservedAt time.Time `json:"lastObservedAt" required:"true"`
|
||||
ServiceAccountID valuer.UUID `json:"serviceAccountId" required:"true"`
|
||||
}
|
||||
|
||||
type GettableFactorAPIKeyWithKey struct {
|
||||
@@ -47,19 +49,19 @@ type GettableFactorAPIKey struct {
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Name string `json:"name" requrired:"true"`
|
||||
ExpiresAt uint64 `json:"expires_at" required:"true"`
|
||||
LastUsed time.Time `json:"last_used" required:"true"`
|
||||
ServiceAccountID valuer.UUID `json:"service_account_id" required:"true"`
|
||||
ExpiresAt uint64 `json:"expiresAt" required:"true"`
|
||||
LastObservedAt time.Time `json:"lastObservedAt" required:"true"`
|
||||
ServiceAccountID valuer.UUID `json:"serviceAccountId" required:"true"`
|
||||
}
|
||||
|
||||
type PostableFactorAPIKey struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
ExpiresAt uint64 `json:"expires_at" required:"true"`
|
||||
ExpiresAt uint64 `json:"expiresAt" required:"true"`
|
||||
}
|
||||
|
||||
type UpdatableFactorAPIKey struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
ExpiresAt uint64 `json:"expires_at" required:"true"`
|
||||
ExpiresAt uint64 `json:"expiresAt" required:"true"`
|
||||
}
|
||||
|
||||
func NewFactorAPIKeyFromStorable(storable *StorableFactorAPIKey) *FactorAPIKey {
|
||||
@@ -69,7 +71,7 @@ func NewFactorAPIKeyFromStorable(storable *StorableFactorAPIKey) *FactorAPIKey {
|
||||
Name: storable.Name,
|
||||
Key: storable.Key,
|
||||
ExpiresAt: storable.ExpiresAt,
|
||||
LastUsed: storable.LastUsed,
|
||||
LastObservedAt: storable.LastObservedAt,
|
||||
ServiceAccountID: valuer.MustNewUUID(storable.ServiceAccountID),
|
||||
}
|
||||
}
|
||||
@@ -91,7 +93,7 @@ func NewStorableFactorAPIKey(factorAPIKey *FactorAPIKey) *StorableFactorAPIKey {
|
||||
Name: factorAPIKey.Name,
|
||||
Key: factorAPIKey.Key,
|
||||
ExpiresAt: factorAPIKey.ExpiresAt,
|
||||
LastUsed: factorAPIKey.LastUsed,
|
||||
LastObservedAt: factorAPIKey.LastObservedAt,
|
||||
ServiceAccountID: factorAPIKey.ServiceAccountID.String(),
|
||||
}
|
||||
}
|
||||
@@ -105,7 +107,7 @@ func NewGettableFactorAPIKeys(keys []*FactorAPIKey) []*GettableFactorAPIKey {
|
||||
TimeAuditable: key.TimeAuditable,
|
||||
Name: key.Name,
|
||||
ExpiresAt: key.ExpiresAt,
|
||||
LastUsed: key.LastUsed,
|
||||
LastObservedAt: key.LastObservedAt,
|
||||
ServiceAccountID: key.ServiceAccountID,
|
||||
}
|
||||
}
|
||||
@@ -128,6 +130,29 @@ func (apiKey *FactorAPIKey) Update(name string, expiresAt uint64) {
|
||||
apiKey.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
func (apiKey *FactorAPIKey) IsExpired() error {
|
||||
if apiKey.ExpiresAt == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if time.Now().After(time.Unix(int64(apiKey.ExpiresAt), 0)) {
|
||||
return errors.New(errors.TypeUnauthenticated, ErrCodeAPIKeyExpired, "api key has been expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (apiKey *FactorAPIKey) UpdateLastObservedAt(lastObservedAt time.Time) error {
|
||||
if lastObservedAt.Before(apiKey.LastObservedAt) {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeAPIkeyOlderLastObservedAt, "last observed at is before the current last observed at")
|
||||
}
|
||||
|
||||
apiKey.LastObservedAt = lastObservedAt
|
||||
apiKey.UpdatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (key *PostableFactorAPIKey) UnmarshalJSON(data []byte) error {
|
||||
type Alias PostableFactorAPIKey
|
||||
|
||||
@@ -137,7 +162,7 @@ func (key *PostableFactorAPIKey) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
if temp.Name == "" {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeServiceAccountFactorAPIkeyInvalidInput, "name cannot be empty")
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeAPIkeyInvalidInput, "name cannot be empty")
|
||||
}
|
||||
|
||||
*key = PostableFactorAPIKey(temp)
|
||||
@@ -153,7 +178,7 @@ func (key *UpdatableFactorAPIKey) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
if temp.Name == "" {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeServiceAccountFactorAPIkeyInvalidInput, "name cannot be empty")
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeAPIkeyInvalidInput, "name cannot be empty")
|
||||
}
|
||||
|
||||
*key = UpdatableFactorAPIKey(temp)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -15,10 +15,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeServiceAccountInvalidInput = errors.MustNewCode("service_account_invalid_input")
|
||||
ErrCodeServiceAccountAlreadyExists = errors.MustNewCode("service_account_already_exists")
|
||||
ErrCodeServiceAccountNotFound = errors.MustNewCode("service_account_not_found")
|
||||
ErrCodeServiceAccountRoleAlreadyExists = errors.MustNewCode("service_account_role_already_exists")
|
||||
ErrCodeServiceAccountInvalidInput = errors.MustNewCode("service_account_invalid_input")
|
||||
ErrCodeServiceAccountAlreadyExists = errors.MustNewCode("service_account_already_exists")
|
||||
ErrCodeServiceAccountNotFound = errors.MustNewCode("service_account_not_found")
|
||||
ErrCodeServiceAccountRoleAlreadyExists = errors.MustNewCode("service_account_role_already_exists")
|
||||
ErrCodeServiceAccountOperationUnsupported = errors.MustNewCode("service_account_operation_unsupported")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,25 +28,31 @@ var (
|
||||
ValidStatus = []valuer.String{StatusActive, StatusDisabled}
|
||||
)
|
||||
|
||||
var (
|
||||
serviceAccountNameRegex = regexp.MustCompile("^[a-z-]{1,50}$")
|
||||
)
|
||||
|
||||
type StorableServiceAccount struct {
|
||||
bun.BaseModel `bun:"table:service_account,alias:service_account"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Name string `bun:"name"`
|
||||
Email string `bun:"email"`
|
||||
Status valuer.String `bun:"status"`
|
||||
OrgID string `bun:"org_id"`
|
||||
Name string `bun:"name"`
|
||||
Email string `bun:"email"`
|
||||
Status valuer.String `bun:"status"`
|
||||
OrgID string `bun:"org_id"`
|
||||
DeletedAt time.Time `bun:"deleted_at"`
|
||||
}
|
||||
|
||||
type ServiceAccount struct {
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Name string `json:"name" required:"true"`
|
||||
Email valuer.Email `json:"email" required:"true"`
|
||||
Roles []string `json:"roles" required:"true" nullable:"false"`
|
||||
Status valuer.String `json:"status" required:"true"`
|
||||
OrgID valuer.UUID `json:"orgID" required:"true"`
|
||||
Name string `json:"name" required:"true"`
|
||||
Email valuer.Email `json:"email" required:"true"`
|
||||
Roles []string `json:"roles" required:"true" nullable:"false"`
|
||||
Status valuer.String `json:"status" required:"true"`
|
||||
OrgID valuer.UUID `json:"orgId" required:"true"`
|
||||
DeletedAt time.Time `json:"deletedAt" required:"true"`
|
||||
}
|
||||
|
||||
type PostableServiceAccount struct {
|
||||
@@ -73,11 +80,12 @@ func NewServiceAccount(name string, email valuer.Email, roles []string, status v
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: name,
|
||||
Email: email,
|
||||
Roles: roles,
|
||||
Status: status,
|
||||
OrgID: orgID,
|
||||
Name: name,
|
||||
Email: email,
|
||||
Roles: roles,
|
||||
Status: status,
|
||||
OrgID: orgID,
|
||||
DeletedAt: time.Time{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +98,7 @@ func NewServiceAccountFromStorables(storableServiceAccount *StorableServiceAccou
|
||||
Roles: roles,
|
||||
Status: storableServiceAccount.Status,
|
||||
OrgID: valuer.MustNewUUID(storableServiceAccount.OrgID),
|
||||
DeletedAt: storableServiceAccount.DeletedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,22 +135,46 @@ func NewStorableServiceAccount(serviceAccount *ServiceAccount) *StorableServiceA
|
||||
Email: serviceAccount.Email.String(),
|
||||
Status: serviceAccount.Status,
|
||||
OrgID: serviceAccount.OrgID.String(),
|
||||
DeletedAt: serviceAccount.DeletedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (sa *ServiceAccount) Update(name string, email valuer.Email, roles []string) {
|
||||
func (sa *ServiceAccount) Update(name string, email valuer.Email, roles []string) error {
|
||||
if err := sa.ErrIfDisabled(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sa.Name = name
|
||||
sa.Email = email
|
||||
sa.Roles = roles
|
||||
sa.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sa *ServiceAccount) UpdateStatus(status valuer.String) {
|
||||
func (sa *ServiceAccount) UpdateStatus(status valuer.String) error {
|
||||
if err := sa.ErrIfDisabled(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sa.Status = status
|
||||
sa.UpdatedAt = time.Now()
|
||||
sa.DeletedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sa *ServiceAccount) ErrIfDisabled() error {
|
||||
if sa.Status == StatusDisabled {
|
||||
return errors.New(errors.TypeUnsupported, ErrCodeServiceAccountOperationUnsupported, "this operation is not supported for disabled service account")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sa *ServiceAccount) NewFactorAPIKey(name string, expiresAt uint64) (*FactorAPIKey, error) {
|
||||
if err := sa.ErrIfDisabled(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
@@ -161,7 +194,7 @@ func (sa *ServiceAccount) NewFactorAPIKey(name string, expiresAt uint64) (*Facto
|
||||
Name: name,
|
||||
Key: encodedKey,
|
||||
ExpiresAt: expiresAt,
|
||||
LastUsed: time.Now(),
|
||||
LastObservedAt: time.Now(),
|
||||
ServiceAccountID: sa.ID,
|
||||
}, nil
|
||||
}
|
||||
@@ -204,8 +237,8 @@ func (sa *PostableServiceAccount) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if temp.Name == "" {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeServiceAccountInvalidInput, "name cannot be empty")
|
||||
if match := serviceAccountNameRegex.MatchString(temp.Name); !match {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeServiceAccountInvalidInput, "name must conform to the regex: %s", serviceAccountNameRegex.String())
|
||||
}
|
||||
|
||||
if len(temp.Roles) == 0 {
|
||||
@@ -224,8 +257,8 @@ func (sa *UpdatableServiceAccount) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if temp.Name == "" {
|
||||
return errors.New(errors.TypeInvalidInput, ErrCodeServiceAccountInvalidInput, "name cannot be empty")
|
||||
if match := serviceAccountNameRegex.MatchString(temp.Name); !match {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeServiceAccountInvalidInput, "name must conform to the regex: %s", serviceAccountNameRegex.String())
|
||||
}
|
||||
|
||||
if len(temp.Roles) == 0 {
|
||||
@@ -244,8 +277,8 @@ func (sa *UpdatableServiceAccountStatus) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !slices.Contains(ValidStatus, temp.Status) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeServiceAccountInvalidInput, "invalid status: %s, allowed status are: %v", temp.Status, ValidStatus)
|
||||
if temp.Status != StatusDisabled {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeServiceAccountInvalidInput, "invalid status: %s, allowed status are: %v", temp.Status, StatusDisabled)
|
||||
}
|
||||
|
||||
*sa = UpdatableServiceAccountStatus(temp)
|
||||
|
||||
@@ -10,6 +10,7 @@ type Store interface {
|
||||
// Service Account
|
||||
Create(context.Context, *StorableServiceAccount) error
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableServiceAccount, error)
|
||||
GetActiveByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableServiceAccount, error)
|
||||
GetByID(context.Context, valuer.UUID) (*StorableServiceAccount, error)
|
||||
List(context.Context, valuer.UUID) ([]*StorableServiceAccount, error)
|
||||
Update(context.Context, valuer.UUID, *StorableServiceAccount) error
|
||||
|
||||
Reference in New Issue
Block a user