Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
085caf3
Add plumbing for new metrics endpoint
nicholasmarais1158 Oct 7, 2025
f640083
Align version compatibility logic
nicholasmarais1158 Oct 7, 2025
a466293
Fix mocked responses in stories
nicholasmarais1158 Oct 7, 2025
e0a4bbc
Add new dashboard component
nicholasmarais1158 Oct 7, 2025
4e0c831
Wire-in dashboard component
nicholasmarais1158 Oct 7, 2025
3db2648
Fix lint
nicholasmarais1158 Oct 8, 2025
74dfdca
Explain dynamic `refetchInterval`
nicholasmarais1158 Oct 10, 2025
0d9c37e
docs: `onFilterSelected`
nicholasmarais1158 Oct 10, 2025
4225ec7
Use typography components from design package
nicholasmarais1158 Oct 13, 2025
146242f
Fix `onFilterSelected` naming inconsistencies
nicholasmarais1158 Oct 13, 2025
9eb6cd6
A better nbsp
nicholasmarais1158 Oct 13, 2025
93ebb27
Remove "control plane" terminology
nicholasmarais1158 Oct 13, 2025
dab2e96
Refactor `GetBotInstanceMetricsResponse` type
nicholasmarais1158 Oct 13, 2025
530718b
Handle out-of-date proxy
nicholasmarais1158 Oct 13, 2025
1a785e3
Make instance list messaging filter aware
nicholasmarais1158 Oct 13, 2025
6688845
Update chart title to "version compatibility"
nicholasmarais1158 Oct 13, 2025
ec07e8c
Keep "Last updated x minutes ago" label current
nicholasmarais1158 Oct 13, 2025
448debf
Oops, forgot to update the test
nicholasmarais1158 Oct 13, 2025
6ac8c23
Merge branch 'master' into nicholasmarais1158/feat/mwi-bot-instance-u…
nicholasmarais1158 Oct 13, 2025
72d9d3a
Merge branch 'master' into nicholasmarais1158/feat/mwi-bot-instance-u…
nicholasmarais1158 Oct 16, 2025
72231cb
Remove unused `TitleText`
nicholasmarais1158 Oct 17, 2025
11cb7ea
Change dashboard title to "insights"
nicholasmarais1158 Oct 17, 2025
2ff479e
Version compatibility design changes
nicholasmarais1158 Oct 17, 2025
6f64297
Merge branch 'master' into nicholasmarais1158/feat/mwi-bot-instance-u…
nicholasmarais1158 Oct 17, 2025
f7c81b0
Fix tests after copy change, oops
nicholasmarais1158 Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions web/packages/teleport/src/BotInstances/BotInstances.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { TeleportProviderBasic } from 'teleport/mocks/providers';
import { defaultAccess, makeAcl } from 'teleport/services/user/makeAcl';
import {
getBotInstanceError,
getBotInstanceMetricsSuccess,
getBotInstanceSuccess,
listBotInstancesError,
listBotInstancesForever,
Expand All @@ -50,7 +51,7 @@ type Story = StoryObj<typeof meta>;

export default meta;

const listBotInstancesSuccessHandler = listBotInstancesSuccess({
const listBotInstances = {
bot_instances: [
{
bot_name: 'ansible-worker',
Expand Down Expand Up @@ -94,13 +95,14 @@ const listBotInstancesSuccessHandler = listBotInstancesSuccess({
},
],
next_page_token: '',
});
};

export const Happy: Story = {
parameters: {
msw: {
handlers: [
listBotInstancesSuccessHandler,
listBotInstancesSuccess(listBotInstances, 'v1'),
listBotInstancesSuccess(listBotInstances, 'v2'),
getBotInstanceSuccess({
bot_instance: {
spec: {
Expand All @@ -109,6 +111,7 @@ export const Happy: Story = {
},
yaml: 'kind: bot_instance\nversion: v1\n',
}),
getBotInstanceMetricsSuccess(),
],
},
},
Expand All @@ -117,15 +120,18 @@ export const Happy: Story = {
export const ErrorLoadingList: Story = {
parameters: {
msw: {
handlers: [listBotInstancesError(500, 'something went wrong')],
handlers: [
listBotInstancesError(500, 'something went wrong'),
getBotInstanceMetricsSuccess(),
],
},
},
};

export const StillLoadingList: Story = {
parameters: {
msw: {
handlers: [listBotInstancesForever()],
handlers: [listBotInstancesForever(), getBotInstanceMetricsSuccess()],
},
},
};
Expand All @@ -141,6 +147,7 @@ export const NoListPermission: Story = {
500,
'this call should never be made without permissions'
),
getBotInstanceMetricsSuccess(),
],
},
},
Expand All @@ -153,11 +160,13 @@ export const NoReadPermission: Story = {
parameters: {
msw: {
handlers: [
listBotInstancesSuccessHandler,
listBotInstancesSuccess(listBotInstances, 'v1'),
listBotInstancesSuccess(listBotInstances, 'v2'),
getBotInstanceError(
500,
'this call should never be made without permissions'
),
getBotInstanceMetricsSuccess(),
],
},
},
Expand Down
177 changes: 123 additions & 54 deletions web/packages/teleport/src/BotInstances/BotInstances.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { createTeleportContext } from 'teleport/mocks/contexts';
import { listBotInstances } from 'teleport/services/bot/bot';
import { defaultAccess, makeAcl } from 'teleport/services/user/makeAcl';
import {
getBotInstanceMetricsSuccess,
getBotInstanceSuccess,
listBotInstancesError,
listBotInstancesSuccess,
Expand All @@ -58,6 +59,9 @@ jest.mock('teleport/services/bot/bot', () => {
getBotInstance: jest.fn((...all) => {
return actual.getBotInstance(...all);
}),
getBotInstanceMetrics: jest.fn((...all) => {
return actual.getBotInstanceMetrics(...all);
}),
};
});

Expand All @@ -67,10 +71,6 @@ beforeAll(() => {
server.listen();
});

beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2025-05-19T08:00:00Z'));
});

afterEach(async () => {
server.resetHandlers();
await testQueryClient.resetQueries();
Expand All @@ -84,11 +84,15 @@ afterAll(() => server.close());
describe('BotInstances', () => {
it('Shows an empty state', async () => {
server.use(
listBotInstancesSuccess({
bot_instances: [],
next_page_token: '',
})
listBotInstancesSuccess(
{
bot_instances: [],
next_page_token: '',
},
'v1'
)
);
server.use(getBotInstanceMetricsSuccess());

renderComponent();

Expand All @@ -104,6 +108,7 @@ describe('BotInstances', () => {

it('Shows an error state', async () => {
server.use(listBotInstancesError(500, 'something went wrong'));
server.use(getBotInstanceMetricsSuccess());

renderComponent();

Expand All @@ -116,6 +121,7 @@ describe('BotInstances', () => {
const testErrorMessage =
'unsupported sort, only bot_name:asc is supported, but got "blah" (desc = true)';
server.use(listBotInstancesError(400, testErrorMessage));
server.use(getBotInstanceMetricsSuccess());

const { user } = renderComponent();

Expand All @@ -124,14 +130,15 @@ describe('BotInstances', () => {
expect(screen.getByText(testErrorMessage)).toBeInTheDocument();

server.use(
listBotInstancesSuccess({
bot_instances: [],
next_page_token: '',
})
listBotInstancesSuccess(
{
bot_instances: [],
next_page_token: '',
},
'v1'
)
);

jest.useRealTimers(); // Required as userEvent.type() uses setTimeout internally

const resetButton = screen.getByRole('button', { name: 'Reset sort' });
await user.click(resetButton);

Expand Down Expand Up @@ -159,25 +166,31 @@ describe('BotInstances', () => {
});

it('Shows a list', async () => {
jest.useFakeTimers().setSystemTime(new Date('2025-05-19T08:00:00Z'));

server.use(
listBotInstancesSuccess({
bot_instances: [
{
bot_name: 'test-bot-1',
instance_id: '5e885c66-1af3-4a36-987d-a604d8ee49d2',
active_at_latest: '2025-05-19T07:32:00Z',
host_name_latest: 'test-hostname',
join_method_latest: 'github',
version_latest: '1.0.0-dev-a12b3c',
},
{
bot_name: 'test-bot-2',
instance_id: '3c3aae3e-de25-4824-a8e9-5a531862f19a',
},
],
next_page_token: '',
})
listBotInstancesSuccess(
{
bot_instances: [
{
bot_name: 'test-bot-1',
instance_id: '5e885c66-1af3-4a36-987d-a604d8ee49d2',
active_at_latest: '2025-05-19T07:32:00Z',
host_name_latest: 'test-hostname',
join_method_latest: 'github',
version_latest: '1.0.0-dev-a12b3c',
},
{
bot_name: 'test-bot-2',
instance_id: '3c3aae3e-de25-4824-a8e9-5a531862f19a',
},
],
next_page_token: '',
},
'v1'
)
);
server.use(getBotInstanceMetricsSuccess());

renderComponent();

Expand All @@ -191,27 +204,29 @@ describe('BotInstances', () => {
});

it('Selects an item', async () => {
jest.useRealTimers(); // Required as userEvent.type() uses setTimeout internally

server.use(
listBotInstancesSuccess({
bot_instances: [
{
bot_name: 'test-bot-1',
instance_id: '5e885c66-1af3-4a36-987d-a604d8ee49d2',
active_at_latest: '2025-05-19T07:32:00Z',
host_name_latest: 'test-hostname',
join_method_latest: 'github',
version_latest: '1.0.0-dev-a12b3c',
},
{
bot_name: 'test-bot-2',
instance_id: '3c3aae3e-de25-4824-a8e9-5a531862f19a',
},
],
next_page_token: '',
})
listBotInstancesSuccess(
{
bot_instances: [
{
bot_name: 'test-bot-1',
instance_id: '5e885c66-1af3-4a36-987d-a604d8ee49d2',
active_at_latest: '2025-05-19T07:32:00Z',
host_name_latest: 'test-hostname',
join_method_latest: 'github',
version_latest: '1.0.0-dev-a12b3c',
},
{
bot_name: 'test-bot-2',
instance_id: '3c3aae3e-de25-4824-a8e9-5a531862f19a',
},
],
next_page_token: '',
},
'v1'
)
);
server.use(getBotInstanceMetricsSuccess());

server.use(
getBotInstanceSuccess({
Expand Down Expand Up @@ -251,7 +266,7 @@ describe('BotInstances', () => {
});

it('Allows paging', async () => {
jest.useRealTimers(); // Required as userEvent.type() uses setTimeout internally
server.use(getBotInstanceMetricsSuccess());

jest.mocked(listBotInstances).mockImplementation(
({ pageToken }) =>
Expand Down Expand Up @@ -327,7 +342,7 @@ describe('BotInstances', () => {
});

it('Allows filtering (search)', async () => {
jest.useRealTimers(); // Required as userEvent.type() uses setTimeout internally
server.use(getBotInstanceMetricsSuccess());

jest.mocked(listBotInstances).mockImplementation(
({ pageToken }) =>
Expand Down Expand Up @@ -407,7 +422,7 @@ describe('BotInstances', () => {
});

it('Allows filtering (query)', async () => {
jest.useRealTimers(); // Required as userEvent.type() uses setTimeout internally
server.use(getBotInstanceMetricsSuccess());

jest.mocked(listBotInstances).mockImplementation(
({ pageToken }) =>
Expand Down Expand Up @@ -491,8 +506,62 @@ describe('BotInstances', () => {
);
});

it('Allows a filter to be applied from the dashboard', async () => {
server.use(getBotInstanceMetricsSuccess());

jest.mocked(listBotInstances).mockImplementation(
({ pageToken }) =>
new Promise(resolve => {
resolve({
bot_instances: [],
next_page_token: pageToken + '.next',
});
})
);

const { user, history } = renderComponent();
jest.spyOn(history, 'push');

await waitForElementToBeRemoved(() =>
screen.queryByTestId('loading-dashboard')
);

expect(listBotInstances).toHaveBeenCalledTimes(1);
expect(listBotInstances).toHaveBeenLastCalledWith(
{
pageSize: 32,
pageToken: '',
searchTerm: '',
query: undefined,
sortDir: 'DESC',
sortField: 'active_at_latest',
},
expect.anything()
);

const item = screen.getByLabelText('Up to date');
await user.click(item);

expect(history.push).toHaveBeenLastCalledWith({
pathname: '/web/bots/instances',
search: 'query=up+to+date+filter+goes+here&is_advanced=1',
});
expect(listBotInstances).toHaveBeenCalledTimes(2);
expect(listBotInstances).toHaveBeenLastCalledWith(
{
pageSize: 32,
pageToken: '', // Should reset to the first page
searchTerm: undefined,
query: 'up to date filter goes here',
sortDir: 'DESC',
sortField: 'active_at_latest',
},
expect.anything()
);
});

it('Allows sorting', async () => {
jest.useRealTimers(); // Required as userEvent.type() uses setTimeout internally
server.use(getBotInstanceMetricsSuccess());

jest.mocked(listBotInstances).mockImplementation(
({ pageToken }) =>
Expand Down
Loading
Loading