Skip to content

Commit 86f4e23

Browse files
cezudasCopilot
andauthored
Add register analytics dashboard service and self-initializing registry infrastructure (#2015)
Fixes OPS-3554. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3d8ba1c commit 86f4e23

2 files changed

Lines changed: 266 additions & 0 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { fetchFinopsDashboardEmbedDetails } from '@openops/common';
2+
import { logger } from '@openops/server-shared';
3+
import {
4+
AnalyticsDashboard,
5+
AnalyticsDashboardRegistry,
6+
FlagId,
7+
OPENOPS_ANALYTICS_FINOPS_SLUG,
8+
} from '@openops/shared';
9+
import { databaseConnection } from '../database/database-connection';
10+
import { FlagEntity } from '../flags/flag.entity';
11+
12+
async function saveAnalyticsDashboardRegistry(
13+
registry: AnalyticsDashboardRegistry,
14+
): Promise<void> {
15+
const flagRepo = databaseConnection().getRepository(FlagEntity);
16+
await flagRepo.save({ id: FlagId.ANALYTICS_DASHBOARDS, value: registry });
17+
}
18+
19+
async function getAnalyticsDashboardRegistry(): Promise<
20+
AnalyticsDashboardRegistry | undefined
21+
> {
22+
const flagRepo = databaseConnection().getRepository(FlagEntity);
23+
const flag = await flagRepo.findOneBy({ id: FlagId.ANALYTICS_DASHBOARDS });
24+
return flag?.value as AnalyticsDashboardRegistry | undefined;
25+
}
26+
27+
async function buildFinopsDashboardRegistryEntry(
28+
accessToken: string,
29+
): Promise<AnalyticsDashboard> {
30+
const {
31+
result: { uuid: embedId },
32+
} = await fetchFinopsDashboardEmbedDetails(accessToken);
33+
return {
34+
id: OPENOPS_ANALYTICS_FINOPS_SLUG,
35+
name: 'FinOps',
36+
slug: OPENOPS_ANALYTICS_FINOPS_SLUG,
37+
embedId,
38+
enabled: true,
39+
};
40+
}
41+
42+
export async function upsertDashboard(
43+
entry: AnalyticsDashboard,
44+
accessToken: string,
45+
): Promise<void> {
46+
const registry = await getAnalyticsDashboardRegistry();
47+
48+
if (!registry) {
49+
const finopsEntry = await buildFinopsDashboardRegistryEntry(accessToken);
50+
const dashboards =
51+
entry.id === OPENOPS_ANALYTICS_FINOPS_SLUG
52+
? [entry]
53+
: [finopsEntry, entry];
54+
await saveAnalyticsDashboardRegistry({
55+
dashboards,
56+
defaultDashboardId: OPENOPS_ANALYTICS_FINOPS_SLUG,
57+
});
58+
59+
logger.info('Analytics dashboard registry created', {
60+
dashboardId: entry.id,
61+
});
62+
63+
return;
64+
}
65+
66+
const existingIndex = registry.dashboards.findIndex((d) => d.id === entry.id);
67+
if (existingIndex >= 0) {
68+
registry.dashboards[existingIndex] = entry;
69+
} else {
70+
registry.dashboards.push(entry);
71+
}
72+
73+
await saveAnalyticsDashboardRegistry(registry);
74+
75+
logger.info('Updated analytics dashboard registry', {
76+
dashboardId: entry.id,
77+
});
78+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
const mockFlagRepo = {
2+
findOneBy: jest.fn(),
3+
save: jest.fn(),
4+
};
5+
6+
const mockDatabaseConnection = {
7+
getRepository: jest.fn().mockReturnValue(mockFlagRepo),
8+
};
9+
10+
jest.mock('../../../src/app/database/database-connection', () => ({
11+
databaseConnection: jest.fn().mockReturnValue(mockDatabaseConnection),
12+
}));
13+
14+
jest.mock('@openops/server-shared', () => {
15+
const originalModule = jest.requireActual('@openops/server-shared');
16+
return {
17+
__esModule: true,
18+
...originalModule,
19+
logger: {
20+
info: jest.fn(),
21+
warn: jest.fn(),
22+
error: jest.fn(),
23+
},
24+
system: {
25+
...originalModule.system,
26+
getBoolean: jest.fn(() => true),
27+
},
28+
};
29+
});
30+
31+
const mockFetchFinopsDashboardEmbedDetails = jest.fn();
32+
jest.mock('@openops/common', () => ({
33+
fetchFinopsDashboardEmbedDetails: mockFetchFinopsDashboardEmbedDetails,
34+
}));
35+
36+
import { AnalyticsDashboard } from '@openops/shared';
37+
import { upsertDashboard } from '../../../src/app/openops-analytics/analytics-dashboard-registry-service';
38+
39+
const ACCESS_TOKEN = 'test-access-token';
40+
41+
const mockFinopsEntry: AnalyticsDashboard = {
42+
id: 'finops',
43+
name: 'FinOps',
44+
slug: 'finops',
45+
embedId: 'finops-embed-uuid',
46+
enabled: true,
47+
};
48+
49+
const mockEntry: AnalyticsDashboard = {
50+
id: 'aws_benchmark',
51+
name: 'AWS Benchmark',
52+
slug: 'aws_benchmark',
53+
embedId: 'benchmark-uuid',
54+
enabled: true,
55+
};
56+
57+
describe('upsertDashboard', () => {
58+
beforeEach(() => {
59+
jest.clearAllMocks();
60+
mockFetchFinopsDashboardEmbedDetails.mockResolvedValue({
61+
result: { uuid: 'finops-embed-uuid' },
62+
});
63+
});
64+
65+
it('creates a new registry with finops as default and the entry when registry does not exist', async () => {
66+
mockFlagRepo.findOneBy.mockResolvedValue(null);
67+
68+
await upsertDashboard(mockEntry, ACCESS_TOKEN);
69+
70+
expect(mockFetchFinopsDashboardEmbedDetails).toHaveBeenCalledWith(
71+
ACCESS_TOKEN,
72+
);
73+
expect(mockFlagRepo.save).toHaveBeenCalledTimes(1);
74+
expect(mockFlagRepo.save).toHaveBeenCalledWith(
75+
expect.objectContaining({
76+
value: {
77+
dashboards: [mockFinopsEntry, mockEntry],
78+
defaultDashboardId: 'finops',
79+
},
80+
}),
81+
);
82+
});
83+
84+
it('updates an existing dashboard entry in the registry without fetching finops', async () => {
85+
const updatedEntry: AnalyticsDashboard = {
86+
...mockEntry,
87+
embedId: 'new-uuid',
88+
};
89+
mockFlagRepo.findOneBy.mockResolvedValue({
90+
id: 'analytics-dashboards',
91+
value: {
92+
dashboards: [mockFinopsEntry, mockEntry],
93+
defaultDashboardId: 'finops',
94+
},
95+
});
96+
97+
await upsertDashboard(updatedEntry, ACCESS_TOKEN);
98+
99+
expect(mockFetchFinopsDashboardEmbedDetails).not.toHaveBeenCalled();
100+
expect(mockFlagRepo.save).toHaveBeenCalledTimes(1);
101+
expect(mockFlagRepo.save).toHaveBeenCalledWith(
102+
expect.objectContaining({
103+
value: {
104+
dashboards: [mockFinopsEntry, updatedEntry],
105+
defaultDashboardId: 'finops',
106+
},
107+
}),
108+
);
109+
});
110+
111+
it('updates only the matching dashboard and leaves other dashboards unchanged', async () => {
112+
const otherDashboard: AnalyticsDashboard = {
113+
id: 'other',
114+
name: 'Other Dashboard',
115+
slug: 'other',
116+
embedId: 'other-uuid',
117+
enabled: true,
118+
};
119+
const updatedEntry: AnalyticsDashboard = {
120+
...mockEntry,
121+
embedId: 'new-uuid',
122+
};
123+
mockFlagRepo.findOneBy.mockResolvedValue({
124+
id: 'analytics-dashboards',
125+
value: {
126+
dashboards: [mockEntry, otherDashboard],
127+
defaultDashboardId: mockEntry.id,
128+
},
129+
});
130+
131+
await upsertDashboard(updatedEntry, ACCESS_TOKEN);
132+
133+
expect(mockFlagRepo.save).toHaveBeenCalledWith(
134+
expect.objectContaining({
135+
value: {
136+
dashboards: [updatedEntry, otherDashboard],
137+
defaultDashboardId: mockEntry.id,
138+
},
139+
}),
140+
);
141+
});
142+
143+
it('creates a registry with only one entry when the entry is the FinOps dashboard', async () => {
144+
mockFlagRepo.findOneBy.mockResolvedValue(null);
145+
146+
await upsertDashboard(mockFinopsEntry, ACCESS_TOKEN);
147+
148+
expect(mockFlagRepo.save).toHaveBeenCalledTimes(1);
149+
expect(mockFlagRepo.save).toHaveBeenCalledWith(
150+
expect.objectContaining({
151+
value: {
152+
dashboards: [mockFinopsEntry],
153+
defaultDashboardId: 'finops',
154+
},
155+
}),
156+
);
157+
});
158+
159+
it('appends a new dashboard entry to an existing registry without fetching finops', async () => {
160+
const otherEntry: AnalyticsDashboard = {
161+
id: 'other',
162+
name: 'Other Dashboard',
163+
slug: 'other',
164+
embedId: 'other-uuid',
165+
enabled: true,
166+
};
167+
mockFlagRepo.findOneBy.mockResolvedValue({
168+
id: 'analytics-dashboards',
169+
value: {
170+
dashboards: [mockFinopsEntry, mockEntry],
171+
defaultDashboardId: 'finops',
172+
},
173+
});
174+
175+
await upsertDashboard(otherEntry, ACCESS_TOKEN);
176+
177+
expect(mockFetchFinopsDashboardEmbedDetails).not.toHaveBeenCalled();
178+
expect(mockFlagRepo.save).toHaveBeenCalledTimes(1);
179+
expect(mockFlagRepo.save).toHaveBeenCalledWith(
180+
expect.objectContaining({
181+
value: {
182+
dashboards: [mockFinopsEntry, mockEntry, otherEntry],
183+
defaultDashboardId: 'finops',
184+
},
185+
}),
186+
);
187+
});
188+
});

0 commit comments

Comments
 (0)