Skip to content

Commit

Permalink
SKYEDEN-3234 UI for inactive topics (#1937)
Browse files Browse the repository at this point in the history
* SKYEDEN-3234 | Add marking and unmarking commands

* SKYEDEN-3234 | Change to batch upserting

* SKYEDEN-3234 | Implement zk unusted topics repo

* SKYEDEN-3234 | Implement unused topics service

* SKYEDEN-3234 | Add repo for last published message timestamp

* SKYEDEN-3234 | Rename from read to get

* SKYEDEN-3234 | Change last notified to list with timestamps

* SKYEDEN-3234 | Implement unused topics detection job

* SKYEDEN-3234 | Add tests for unused topics detection

* SKYEDEN-3234 | Add detection job test

* SKYEDEN-3234 | Refactor detection job test

* SKYEDEN-3234 | Make unused topics notifier bean optional

* SKYEDEN-3234 | Add scheduling

* SKYEDEN-3234 | Add properties with default values

* SKYEDEN-3234 | Add more logging and refactor

* SKYEDEN-3234 | Rename from unused to inactive

* SKYEDEN-3234 | Handle lack of last published message metrics

* SKYEDEN-3234 | Do not call notifier when no inactive topics

* SKYEDEN-3234 | Fix style

* SKYEDEN-3234 | Add log when detection starts

* SKYEDEN-3234 | Fix style

* SKYEDEN-3234 | Add more logging

* SKYEDEN-3234 | Move leader and config to separate classes

* SKYEDEN-3234 | Change log message

* SKYEDEN-3234 | Remove unnecessary annotation

* SKYEDEN-3234 | Do not update notification timestamps when notification skipped

* SKYEDEN-3234 | Add docs for inactive topics detection

* SKYEDEN-3234 | Add notification result as return type of notifier

* SKYEDEN-3234 | Add owner info to be used by notifier

* SKYEDEN-3234 | Limit number of notification timestamps in history

* SKYEDEN-3234 | Update docs

* SKYEDEN-3234 | Specify shorter names to be saved in json

* SKYEDEN-3271 |hermes management leader (#1934)

* SKYEDEN-3271 |hermes management leader

* SKYEDEN-3271 | cr changes

* SKYEDEN-3234 | Remove unused property

* SKYEDEN-3271 | fix leader path

* Revert "Merge branch 'master' into SKYEDEN-3234-detect-unused-topics"

This reverts commit 9545569, reversing
changes made to 8194321.

* Revert "Revert "Merge branch 'master' into SKYEDEN-3234-detect-unused-topics""

This reverts commit cf17940.

* SKYEDEN-3234 | Ui for inactive topics WIP

* SKYEDEN-3234 | Change v-table to v-data-table

* SKYEDEN-3234 | Add link to inactive topics

* SKYEDEN-3234 | Add endpoint for inactive topics

* SKYEDEN-3234 | show error when cannot fetch inactive topics

* SKYEDEN-3234 | linting

* SKYEDEN-3234 | Remove unnecessary fields

* SKYEDEN-3234 | Add test for InactiveTopicsView

* SKYEDEN-3234 | Add test for InactiveTopicsListing

* SKYEDEN-3234 | Add test for UseInactiveTopics

---------

Co-authored-by: Marcin Bobiński <49727204+MarcinBobinski@users.noreply.github.com>
Co-authored-by: Marcin Bobinski <marcin.bobinski@allegro.pl>
  • Loading branch information
3 people authored Dec 20, 2024
1 parent ffc4e39 commit 2a33ada
Show file tree
Hide file tree
Showing 16 changed files with 474 additions and 1 deletion.
5 changes: 5 additions & 0 deletions hermes-console/json-server/db.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"inactiveTopics": [
{"topic": "group.topic1", "lastPublishedTsMs": 1732499845200, "notificationTsMs": [1733499835210, 1733499645212], "whitelisted": false},
{"topic": "group.topic2", "lastPublishedTsMs": 1633928665148, "notificationTsMs": [], "whitelisted": true},
{"topic": "pl.allegro.public.group.DummyEvent", "lastPublishedTsMs": 1633928665148, "notificationTsMs": [1733499645212], "whitelisted": false}
],
"groups": [
"pl.allegro.public.offer",
"pl.allegro.public.offer.product",
Expand Down
3 changes: 2 additions & 1 deletion hermes-console/json-server/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@
"/owners/sources": "/ownerSources",
"/owners/sources/*?search=:searchPhrase": "/topicsOwners?name_like=:searchPhrase",
"/dashboards/topics/:topicName": "/topicDashboardUrl",
"/dashboards/topics/:topicName/subscriptions/:id": "/subscriptionDashboardUrl"
"/dashboards/topics/:topicName/subscriptions/:id": "/subscriptionDashboardUrl",
"/inactive-topics": "/inactiveTopics"
}
5 changes: 5 additions & 0 deletions hermes-console/src/api/hermes-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
Readiness,
} from '@/api/datacenter-readiness';
import type { Group } from '@/api/group';
import type { InactiveTopic } from '@/api/inactive-topics';
import type { InconsistentGroup } from '@/api/inconsistent-group';
import type {
MessageFiltersVerification,
Expand Down Expand Up @@ -199,6 +200,10 @@ export function fetchConstraints(): ResponsePromise<ConstraintsConfig> {
return axios.get<ConstraintsConfig>('/workload-constraints');
}

export function fetchInactiveTopics(): ResponsePromise<InactiveTopic[]> {
return axios.get<InactiveTopic[]>('/inactive-topics');
}

export function fetchReadiness(): ResponsePromise<DatacenterReadiness[]> {
return axios.get<DatacenterReadiness[]>('/readiness/datacenters');
}
Expand Down
6 changes: 6 additions & 0 deletions hermes-console/src/api/inactive-topics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface InactiveTopic {
topic: string;
lastPublishedTsMs: number;
notificationTsMs: number[];
whitelisted: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { afterEach, expect } from 'vitest';
import { createTestingPinia } from '@pinia/testing';
import { dummyInactiveTopics } from '@/dummy/inactiveTopics';
import {
fetchInactiveTopicsErrorHandler,
fetchInactiveTopicsHandler,
} from '@/mocks/handlers';
import { setActivePinia } from 'pinia';
import { setupServer } from 'msw/node';
import { useInactiveTopics } from '@/composables/inactive-topics/use-inactive-topics/useInactiveTopics';
import { waitFor } from '@testing-library/vue';

describe('useInactiveTopics', () => {
const server = setupServer(
fetchInactiveTopicsHandler({ inactiveTopics: dummyInactiveTopics }),
);

const pinia = createTestingPinia({
fakeApp: true,
});

beforeEach(() => {
setActivePinia(pinia);
});

afterEach(() => {
server.resetHandlers();
});

it('should fetch inactive topics from Hermes backend', async () => {
// given
server.listen();

// when
const { inactiveTopics, loading, error } = useInactiveTopics();

// then
expect(loading.value).toBeTruthy();
expect(error.value.fetchInactiveTopics).toBeNull();

await waitFor(() => {
expect(loading.value).toBeFalsy();
expect(error.value.fetchInactiveTopics).toBeNull();
expect(inactiveTopics.value?.length).toBe(2);
});
});

it('should set error to true on inactive topics endpoint failure', async () => {
// given
server.use(fetchInactiveTopicsErrorHandler({ errorCode: 500 }));
server.listen();

// when
const { error } = useInactiveTopics();

// then
await waitFor(() => {
expect(error.value.fetchInactiveTopics).not.toBeNull();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { fetchInactiveTopics as getInactiveTopics } from '@/api/hermes-client';
import { ref } from 'vue';
import type { InactiveTopic } from '@/api/inactive-topics';
import type { Ref } from 'vue';

export interface UseInactiveTopics {
inactiveTopics: Ref<InactiveTopic[] | undefined>;
loading: Ref<boolean>;
error: Ref<UseInactiveTopicsErrors>;
}

export interface UseInactiveTopicsErrors {
fetchInactiveTopics: Error | null;
}

export function useInactiveTopics(): UseInactiveTopics {
const inactiveTopics = ref<InactiveTopic[]>();
const error = ref<UseInactiveTopicsErrors>({
fetchInactiveTopics: null,
});
const loading = ref(false);

const fetchInactiveTopics = async () => {
try {
loading.value = true;
inactiveTopics.value = (await getInactiveTopics()).data;
} catch (e) {
error.value.fetchInactiveTopics = e as Error;
} finally {
loading.value = false;
}
};

fetchInactiveTopics();

return {
inactiveTopics: inactiveTopics,
loading,
error,
};
}
16 changes: 16 additions & 0 deletions hermes-console/src/dummy/inactiveTopics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { InactiveTopic } from '@/api/inactive-topics';

export const dummyInactiveTopics: InactiveTopic[] = [
{
topic: 'group.topic1',
lastPublishedTsMs: 1732499845200,
notificationTsMs: [1733499835210, 1733499645212],
whitelisted: false,
},
{
topic: 'group.topic2',
lastPublishedTsMs: 1633928665148,
notificationTsMs: [],
whitelisted: true,
},
];
14 changes: 14 additions & 0 deletions hermes-console/src/i18n/en-US/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ const en_US = {
appliedFilter: '(applied filter: “{filter}”)',
},
},
inactiveTopics: {
connectionError: {
title: 'Connection error',
text: 'Could not fetch information about inactive topics',
},
heading: 'Inactive Topics',
listing: {
name: 'Name',
lastUsed: 'Last published message',
lastNotified: 'Last notified',
howManyTimesNotified: 'How many times notified',
whitelisted: 'Whitelisted',
},
},
stats: {
connectionError: {
title: 'Connection error',
Expand Down
21 changes: 21 additions & 0 deletions hermes-console/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { ConsumerGroup } from '@/api/consumer-group';
import type { DashboardUrl } from '@/composables/metrics/use-metrics/useMetrics';
import type { DatacenterReadiness } from '@/api/datacenter-readiness';
import type { Group } from '@/api/group';
import type { InactiveTopic } from '@/api/inactive-topics';
import type { InconsistentGroup } from '@/api/inconsistent-group';
import type { MessageFiltersVerificationResponse } from '@/api/message-filters-verification';
import type {
Expand Down Expand Up @@ -403,6 +404,26 @@ export const fetchConstraintsErrorHandler = ({
});
});

export const fetchInactiveTopicsHandler = ({
inactiveTopics,
}: {
inactiveTopics: InactiveTopic[];
}) =>
http.get(`${url}/inactive-topics`, () => {
return HttpResponse.json(inactiveTopics);
});

export const fetchInactiveTopicsErrorHandler = ({
errorCode = 500,
}: {
errorCode?: number;
}) =>
http.get(`${url}/inactive-topics`, () => {
return new HttpResponse(undefined, {
status: errorCode,
});
});

export const fetchReadinessHandler = ({
datacentersReadiness,
}: {
Expand Down
6 changes: 6 additions & 0 deletions hermes-console/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ const router = createRouter({
name: 'constraints',
component: () => import('@/views/admin/constraints/ConstraintsView.vue'),
},
{
path: '/ui/inactive-topics',
name: 'inactiveTopics',
component: () =>
import('@/views/admin/inactive-topics/InactiveTopicsView.vue'),
},
{
path: '/ui/consistency',
name: 'consistency',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { dummyInactiveTopics } from '@/dummy/inactiveTopics';
import { expect } from 'vitest';
import { ref } from 'vue';
import { render } from '@/utils/test-utils';
import { useInactiveTopics } from '@/composables/inactive-topics/use-inactive-topics/useInactiveTopics';
import InactiveTopicsView from '@/views/admin/inactive-topics/InactiveTopicsView.vue';
import type { UseInactiveTopics } from '@/composables/inactive-topics/use-inactive-topics/useInactiveTopics';

vi.mock('@/composables/inactive-topics/use-inactive-topics/useInactiveTopics');

const useInactiveTopicsStub: UseInactiveTopics = {
inactiveTopics: ref(dummyInactiveTopics),
loading: ref(false),
error: ref({ fetchInactiveTopics: null }),
};

describe('InactiveTopicsView', () => {
it('should render if inactive topics data was successfully fetched', () => {
// given
vi.mocked(useInactiveTopics).mockReturnValueOnce(useInactiveTopicsStub);

// when
const { getByText } = render(InactiveTopicsView);

// then
expect(vi.mocked(useInactiveTopics)).toHaveBeenCalledOnce();
expect(getByText('inactiveTopics.heading')).toBeVisible();
});

it('should show loading spinner when fetching inactive topics data', () => {
// given
vi.mocked(useInactiveTopics).mockReturnValueOnce({
...useInactiveTopicsStub,
loading: ref(true),
});

// when
const { queryByTestId } = render(InactiveTopicsView);

// then
expect(vi.mocked(useInactiveTopics)).toHaveBeenCalledOnce();
expect(queryByTestId('loading-spinner')).toBeVisible();
});

it('should hide loading spinner when data fetch is complete', () => {
// given
vi.mocked(useInactiveTopics).mockReturnValueOnce({
...useInactiveTopicsStub,
loading: ref(false),
});

// when
const { queryByTestId } = render(InactiveTopicsView);

// then
expect(vi.mocked(useInactiveTopics)).toHaveBeenCalledOnce();
expect(queryByTestId('loading-spinner')).not.toBeInTheDocument();
});

it('should show error message when fetching data failed', () => {
// given
vi.mocked(useInactiveTopics).mockReturnValueOnce({
...useInactiveTopicsStub,
loading: ref(false),
error: ref({ fetchInactiveTopics: new Error() }),
});

// when
const { queryByText } = render(InactiveTopicsView);

// then
expect(vi.mocked(useInactiveTopics)).toHaveBeenCalledOnce();
expect(queryByText('inactiveTopics.connectionError.title')).toBeVisible();
expect(queryByText('inactiveTopics.connectionError.text')).toBeVisible();
});

it('should not show error message when data was fetch successfully', () => {
// given
vi.mocked(useInactiveTopics).mockReturnValueOnce({
...useInactiveTopicsStub,
loading: ref(false),
error: ref({ fetchInactiveTopics: null }),
});

// when
const { queryByText } = render(InactiveTopicsView);

// then
expect(vi.mocked(useInactiveTopics)).toHaveBeenCalledOnce();
expect(
queryByText('inactiveTopics.connectionError.title'),
).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
import { useInactiveTopics } from '@/composables/inactive-topics/use-inactive-topics/useInactiveTopics';
import ConsoleAlert from '@/components/console-alert/ConsoleAlert.vue';
import InactiveTopicsListing from '@/views/admin/inactive-topics/inactive-topics-listing/InactiveTopicsListing.vue';
import LoadingSpinner from '@/components/loading-spinner/LoadingSpinner.vue';
const { inactiveTopics, loading, error } = useInactiveTopics();
</script>

<template>
<v-container>
<v-row dense>
<v-col md="12">
<loading-spinner v-if="loading" />
<console-alert
v-if="error.fetchInactiveTopics"
:title="$t('inactiveTopics.connectionError.title')"
:text="$t('inactiveTopics.connectionError.text')"
type="error"
/>
</v-col>
</v-row>
<v-row dense>
<v-col md="10">
<p class="text-h4 font-weight-bold mb-3">
{{ $t('inactiveTopics.heading') }}
</p>
</v-col>
</v-row>
<v-row dense>
<v-col md="12">
<inactive-topics-listing
v-if="inactiveTopics"
:inactive-topics="inactiveTopics"
/>
</v-col>
</v-row>
</v-container>
</template>

<style scoped lang="scss"></style>
Loading

0 comments on commit 2a33ada

Please sign in to comment.