-
Notifications
You must be signed in to change notification settings - Fork 27
feat: Add useTopics model hook and query for topics listing #136
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,7 +52,7 @@ When('it is rendered with no version', () => { | |
showVersionSet = false; | ||
}); | ||
|
||
Then('it should display the expected text', () => { | ||
Then('it should display the expected text', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wont block the PR on it, but was this intended? |
||
const { getByText, queryByText } = renderResult; | ||
expect(getByText('Welcome to the Strimzi UI')).toBeInTheDocument(); | ||
const versionString = `Version: ${coreConfigFromContext.client.about.version}`; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/* | ||
* Copyright Strimzi authors. | ||
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). | ||
*/ | ||
import { ChangeEvent, useState, useCallback } from 'react'; | ||
import { GET_TOPICS } from 'Queries/Topics'; | ||
import { useQuery } from '@apollo/client'; | ||
import { useTopicsModelType, topicsType } from './useTopicsModel.types'; | ||
import debounce from 'lodash.debounce'; | ||
|
||
const onChangeEvent = ( | ||
evt: ChangeEvent<HTMLInputElement>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use html events here, it breaks the MVVC abstraction. Use scalar types or add an abstraction. |
||
callWithValue: (value: string) => void | ||
) => callWithValue(evt.target.value); | ||
|
||
export const useTopicsModel = (): useTopicsModelType => { | ||
const [filter, setTopicsFilter] = useState(); | ||
const debouncedUpdateTopicsFilter = useCallback( | ||
debounce(setTopicsFilter, 500), | ||
[] | ||
); | ||
const { data, loading, error } = useQuery<topicsType>(GET_TOPICS, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Types start with capital letters. |
||
variables: { filter }, | ||
}); | ||
|
||
const model = { | ||
filter, | ||
topics: data ? data.topics : [], | ||
error, | ||
loading, | ||
}; | ||
|
||
return { | ||
model, | ||
updateTopicsFilter: (evt) => | ||
onChangeEvent(evt, debouncedUpdateTopicsFilter), | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Topics | ||
|
||
The `Topics` panel retrieves and displays the list of Kafka topics for a cluster. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
|
||
# Copyright Strimzi authors. | ||
# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). | ||
Feature: Topics panel | ||
|
||
Scenario: Seeing all available topics: | ||
Given a Topics panel component | ||
When it is rendered | ||
Then I see all topics | ||
|
||
Scenario: Viewing a specific topic (when it exists in the cluster): | ||
Given a Topics panel component | ||
When it is rendered | ||
And I filter for topic 'testtopic1' | ||
Then I see topic 'testtopic1' in the topic list |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,87 @@ | ||||
/* | ||||
* Copyright Strimzi authors. | ||||
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). | ||||
*/ | ||||
import { | ||||
And, | ||||
Given, | ||||
When, | ||||
Then, | ||||
Fusion, | ||||
Before, | ||||
After, | ||||
} from 'jest-cucumber-fusion'; | ||||
import { | ||||
fireEvent, | ||||
render, | ||||
RenderResult, | ||||
waitFor, | ||||
} from '@testing-library/react'; | ||||
import { Topics } from '.'; | ||||
import React, { ReactElement } from 'react'; | ||||
import { withApolloProviderReturning } from 'utils/test'; | ||||
import { | ||||
mockGetTopicsRequests, | ||||
mockGetTopicsResponses, | ||||
} from './useTopicsModel.assets'; | ||||
|
||||
let renderResult: RenderResult; | ||||
let component: ReactElement; | ||||
|
||||
Before(() => { | ||||
jest.useFakeTimers(); | ||||
}); | ||||
|
||||
After(() => { | ||||
jest.useRealTimers(); | ||||
}); | ||||
|
||||
Given('a Topics panel component', () => { | ||||
component = <Topics />; | ||||
}); | ||||
|
||||
When('it is rendered', () => { | ||||
renderResult = render( | ||||
withApolloProviderReturning( | ||||
[ | ||||
mockGetTopicsRequests.successRequest, | ||||
mockGetTopicsRequests.successFilteredRequest, | ||||
], | ||||
component | ||||
) | ||||
); | ||||
jest.runAllTimers(); | ||||
}); | ||||
|
||||
And(/^I filter for topic '(.+)'$/, async (filter) => { | ||||
fireEvent.change(renderResult.getByPlaceholderText('filter'), { | ||||
target: { value: filter }, | ||||
}); | ||||
await waitFor(() => { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My personal preference would be that rather than using mock timers, we use the apollo helpers, eg
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. |
||||
jest.runAllTimers(); | ||||
}); | ||||
}); | ||||
|
||||
Then('I see all topics', async () => { | ||||
const { findByText } = renderResult; | ||||
expect( | ||||
await findByText(JSON.stringify(mockGetTopicsResponses.successResponse), { | ||||
exact: false, | ||||
}) | ||||
).toBeInTheDocument(); | ||||
}); | ||||
|
||||
Then(/^I see topic '(.+)' in the topic list$/, async (topicName) => { | ||||
const { findByText } = renderResult; | ||||
expect( | ||||
await findByText( | ||||
JSON.stringify(mockGetTopicsResponses.successFilteredResponse), | ||||
{ exact: false } | ||||
) | ||||
).toBeInTheDocument(); | ||||
expect( | ||||
await findByText(topicName as string, { exact: false }) | ||||
).toBeInTheDocument(); | ||||
}); | ||||
|
||||
Fusion('Topics.feature'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright Strimzi authors. | ||
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). | ||
*/ | ||
import React, { FunctionComponent } from 'react'; | ||
import { useTopicsModel } from './Model'; | ||
|
||
/** | ||
* Placeholder Topics panel FC. | ||
*/ | ||
const Topics: FunctionComponent = ({ children }) => { | ||
const { model, updateTopicsFilter } = useTopicsModel(); | ||
|
||
let topics: JSX.Element; | ||
if (model.loading) { | ||
topics = <p>Loading...</p>; | ||
} else { | ||
topics = <p>{`Topics retrieved: ${JSON.stringify(model.topics)}`}</p>; | ||
} | ||
|
||
return ( | ||
<div className='topics'> | ||
<input placeholder='filter' onChange={updateTopicsFilter} /> | ||
{topics} | ||
{children} | ||
</div> | ||
); | ||
}; | ||
|
||
export { Topics }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/* | ||
* Copyright Strimzi authors. | ||
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). | ||
*/ | ||
export * from './View.carbon'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/* | ||
* Copyright Strimzi authors. | ||
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). | ||
*/ | ||
import { generateMockDataResponseForGQLRequest } from 'utils/test/withApollo/withApollo.util'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
import { topicType } from './useTopicsModel.types'; | ||
import { GET_TOPICS } from 'Queries/Topics'; | ||
|
||
const successResponse: topicType[] = [ | ||
{ name: 'testtopic1', partitions: 1, replicas: 1 }, | ||
{ name: 'testtopic2', partitions: 2, replicas: 2 }, | ||
{ name: 'testtopic3', partitions: 3, replicas: 3 }, | ||
]; | ||
|
||
const successFilteredResponse: topicType[] = [ | ||
{ name: 'testtopic1', partitions: 1, replicas: 1 }, | ||
]; | ||
|
||
export const mockGetTopicsResponses = { | ||
successResponse, | ||
successFilteredResponse, | ||
}; | ||
|
||
const successRequest = generateMockDataResponseForGQLRequest( | ||
GET_TOPICS, | ||
{ topics: successResponse }, | ||
{ variables: { filter: undefined } } | ||
); | ||
|
||
const successFilteredRequest = generateMockDataResponseForGQLRequest( | ||
GET_TOPICS, | ||
{ topics: successFilteredResponse }, | ||
{ variables: { filter: 'testtopic1' } } | ||
); | ||
|
||
export const mockGetTopicsRequests = { | ||
successRequest, | ||
successFilteredRequest, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* | ||
* Copyright Strimzi authors. | ||
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). | ||
*/ | ||
import React from 'react'; | ||
import { renderHook, act } from '@testing-library/react-hooks'; | ||
import { withApolloProviderReturning } from 'utils/test'; | ||
import { useTopicsModel } from './Model'; | ||
import { | ||
mockGetTopicsRequests, | ||
mockGetTopicsResponses, | ||
} from './useTopicsModel.assets'; | ||
|
||
describe('`useTopicsModel` hook', () => { | ||
beforeEach(() => { | ||
jest.useFakeTimers(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.useRealTimers(); | ||
}); | ||
|
||
it('returns the expected query content', async () => { | ||
const { result, rerender } = renderHook(useTopicsModel, { | ||
wrapper: ({ children }) => | ||
withApolloProviderReturning( | ||
[mockGetTopicsRequests.successRequest], | ||
<React.Fragment>{children}</React.Fragment> | ||
), | ||
}); | ||
|
||
expect(result.current.model.loading).toBe(true); | ||
expect(result.current.model.topics).toEqual([]); | ||
|
||
await act(async () => { | ||
jest.runAllTimers(); | ||
}); | ||
rerender(); | ||
|
||
expect(result.current.model.topics).toEqual( | ||
mockGetTopicsResponses.successResponse | ||
); | ||
expect(result.current.model.loading).toBe(false); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* Copyright Strimzi authors. | ||
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). | ||
*/ | ||
import { ApolloError } from '@apollo/client'; | ||
import { ChangeEvent } from 'react'; | ||
|
||
/** the shape of the object returned by the useTopicsModel hook */ | ||
export type useTopicsModelType = { | ||
model: { | ||
filter: string | undefined; | ||
topics: topicType[]; | ||
error: ApolloError | undefined; | ||
loading: boolean; | ||
}; | ||
updateTopicsFilter: (evt: ChangeEvent<HTMLInputElement>) => void; | ||
}; | ||
|
||
export type topicsType = { | ||
topics: topicType[]; | ||
}; | ||
|
||
export type topicType = { | ||
name: string; | ||
partitions: number; | ||
replicas: number; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the types generated by the Rama's PR here. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/* | ||
* Copyright Strimzi authors. | ||
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). | ||
*/ | ||
export * from './Home'; | ||
export * from './Topics'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs a router, not hard coding like this.