diff --git a/client/Bootstrap/index.tsx b/client/Bootstrap/index.tsx index c9274950..770416b5 100644 --- a/client/Bootstrap/index.tsx +++ b/client/Bootstrap/index.tsx @@ -8,9 +8,12 @@ import { init } from 'i18n'; import { ApolloProvider } from '@apollo/client'; import { apolloClient } from 'Bootstrap/GraphQLClient'; -import { ConfigFeatureFlagProvider, FeatureFlag } from 'Contexts'; +import { + ConfigFeatureFlagProvider, + FeatureFlag, + LoggingProvider, +} from 'Contexts'; import { Home } from 'Panels/Home'; -import { LoggingProvider } from 'Contexts'; init(); //Bootstrap i18next support ReactDOM.render( diff --git a/client/Models/README.md b/client/Models/README.md new file mode 100644 index 00000000..5acc99a6 --- /dev/null +++ b/client/Models/README.md @@ -0,0 +1,37 @@ +# Models + +This directory contains custom [React Hooks](https://reactjs.org/docs/hooks-intro.html#motivation). Unlike the [Hooks](../Hooks/README.md), these hooks encapsulate specific business logic for views to utilise. + +## Test approach + +Elements should be tested in a functional manor. See [Test Driven Development](../../docs/Test.md#style-of-test). + +## Expected files + +For a given model `useFoo`, the expected files are as follows: + +``` +Models/ + index.ts + types.ts + useFoo/ + README.md + useFoo.ts + useFoo.spec.ts + useFoo.assets.ts + useFoo.types.ts +``` + +Where: + +- index.ts acts as a barrel file, exporting the hooks defined in the Hooks directory +- types.ts acts as a barrel file, exporting all the public types of each context +- README.md is the readme for this hook, detailing design choices and usage +- useFoo.ts is the hook implementation +- useFoo.spec.ts are the tests for this hook +- useFoo.assets.ts are the test assets for this hook +- useFoo.types.ts are the types for this hook + +## Implemented hooks + +- [`useTopics`](./useTopics/README.md) - a hook providing sub-hooks to administer Kafka topics via GraphQL queries. diff --git a/client/Models/index.ts b/client/Models/index.ts new file mode 100644 index 00000000..e9772d21 --- /dev/null +++ b/client/Models/index.ts @@ -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 './useTopics'; diff --git a/client/Models/useTopics/Hook.ts b/client/Models/useTopics/Hook.ts new file mode 100644 index 00000000..fb34f6d8 --- /dev/null +++ b/client/Models/useTopics/Hook.ts @@ -0,0 +1,14 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import { GET_TOPICS } from 'Queries/Topics'; +import { useQuery } from '@apollo/client'; +import { useTopicsType } from './useTopics.types'; + +export const useTopics = (): useTopicsType => { + const useGetTopics = () => useQuery(GET_TOPICS); + return { + useGetTopics, + }; +}; diff --git a/client/Models/useTopics/README.md b/client/Models/useTopics/README.md new file mode 100644 index 00000000..3c55eb8b --- /dev/null +++ b/client/Models/useTopics/README.md @@ -0,0 +1,5 @@ +# useTopics + +The `useTopics` hook returns sub-hooks to administer Kafka topics via GraphQL queries to the server `/api` endpoint. The sub-hooks returned by the `useTopics` hook are: + +- `useGetTopics()` - returns the list of topics, including name, partitions count and replicas count, as an [Apollo `QueryResult` object](https://www.apollographql.com/docs/react/api/react/hooks/#result). diff --git a/client/Models/useTopics/index.ts b/client/Models/useTopics/index.ts new file mode 100644 index 00000000..8f06b034 --- /dev/null +++ b/client/Models/useTopics/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +export * from './Hook'; +export * from './useTopics.types'; +export * from './useTopics.assets'; diff --git a/client/Models/useTopics/useTopics.assets.ts b/client/Models/useTopics/useTopics.assets.ts new file mode 100644 index 00000000..aef75719 --- /dev/null +++ b/client/Models/useTopics/useTopics.assets.ts @@ -0,0 +1,24 @@ +/* + * 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'; +import { topicsType } from './useTopics.types'; +import { GET_TOPICS } from 'Queries/Topics'; + +export const mockGetTopicsResponse: topicsType = { + topics: [ + { name: 'testtopic1', partitions: 1, replicas: 1 }, + { name: 'testtopic2', partitions: 2, replicas: 2 }, + { name: 'testtopic3', partitions: 3, replicas: 3 }, + ], +}; + +const successRequest = generateMockDataResponseForGQLRequest( + GET_TOPICS, + mockGetTopicsResponse +); + +export const mockGetTopicsRequests = { + successRequest, +}; diff --git a/client/Models/useTopics/useTopics.spec.tsx b/client/Models/useTopics/useTopics.spec.tsx new file mode 100644 index 00000000..03a81d88 --- /dev/null +++ b/client/Models/useTopics/useTopics.spec.tsx @@ -0,0 +1,40 @@ +/* + * 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, apolloMockResponse } from 'utils/test'; +import { useTopics } from './Hook'; +import { + mockGetTopicsRequests, + mockGetTopicsResponse, +} from './useTopics.assets'; + +describe('`useTopics` hook', () => { + describe('`useGetTopics` hook', () => { + it('returns the expected content', async () => { + const { result, rerender } = renderHook( + () => useTopics().useGetTopics(), + { + wrapper: ({ children }) => + withApolloProviderReturning( + [mockGetTopicsRequests.successRequest], + {children} + ), + } + ); + + expect(result.current.loading).toBe(true); + expect(result.current.data).toBeUndefined(); + + await act(async () => { + await apolloMockResponse(); + }); + rerender(); + + expect(result.current.loading).toBe(false); + expect(result.current.data).toEqual(mockGetTopicsResponse); + }); + }); +}); diff --git a/client/Models/useTopics/useTopics.types.ts b/client/Models/useTopics/useTopics.types.ts new file mode 100644 index 00000000..e1e7bf4c --- /dev/null +++ b/client/Models/useTopics/useTopics.types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import { QueryResult } from '@apollo/client'; + +/** the shape of the object returned by the useTopics hook */ +export type useTopicsType = { + useGetTopics: () => QueryResult; +}; + +/** the shape of the `data` returned by the useGetTopics hook */ +export type topicsType = { + topics: topicType[]; +}; + +export type topicType = { + name: string; + partitions: number; + replicas: number; +}; diff --git a/client/Panels/Home/Home.steps.tsx b/client/Panels/Home/Home.steps.tsx index f39363ad..4be212ad 100644 --- a/client/Panels/Home/Home.steps.tsx +++ b/client/Panels/Home/Home.steps.tsx @@ -5,7 +5,12 @@ import { Given, When, Then, Fusion } from 'jest-cucumber-fusion'; import { RenderResult } from '@testing-library/react'; import merge from 'lodash.merge'; -import { renderWithCustomConfigFeatureFlagContext } from 'utils/test'; +import { + renderWithCustomConfigFeatureFlagContext, + withApolloProviderReturning, + apolloMockResponse, +} from 'utils/test'; +import { mockGetTopicsRequests } from 'Models'; import { Home } from '.'; import React, { ReactElement } from 'react'; @@ -31,7 +36,10 @@ Given('a Home component', () => { When('it is rendered', () => { renderResult = renderWithCustomConfigFeatureFlagContext( coreConfigFromContext, - component + withApolloProviderReturning( + [mockGetTopicsRequests.successRequest], + component + ) ); showVersionSet = true; }); @@ -47,18 +55,28 @@ When('it is rendered with no version', () => { }, }, }), - component + withApolloProviderReturning( + [mockGetTopicsRequests.successRequest], + component + ) ); showVersionSet = false; }); -Then('it should display the expected text', () => { +Then('it should display the expected text', async () => { const { getByText, queryByText } = renderResult; expect(getByText('Welcome to the Strimzi UI')).toBeInTheDocument(); const versionString = `Version: ${coreConfigFromContext.client.about.version}`; showVersionSet ? expect(getByText(versionString)).toBeInTheDocument() : expect(queryByText(versionString)).not.toBeInTheDocument(); + + const loadingTopicsString = 'Loading...'; + expect(getByText(loadingTopicsString)).toBeInTheDocument(); + + await apolloMockResponse(); + expect(queryByText(loadingTopicsString)).not.toBeInTheDocument(); + expect(getByText('Number of topics: 3')).toBeInTheDocument(); }); Fusion('Home.feature'); diff --git a/client/Panels/Home/Home.tsx b/client/Panels/Home/Home.tsx index 08a985e2..63333cd0 100644 --- a/client/Panels/Home/Home.tsx +++ b/client/Panels/Home/Home.tsx @@ -7,6 +7,7 @@ import get from 'lodash.get'; import image from 'Images/logo.png'; import './style.scss'; import { useConfigFeatureFlag, useLogger } from 'Hooks'; +import { useTopics } from 'Models'; const Home: FunctionComponent = ({ children }) => { const { client, featureFlags, isComplete } = useConfigFeatureFlag(); @@ -17,11 +18,19 @@ const Home: FunctionComponent = ({ children }) => { const { debug } = useLogger('Home'); debug(`Client version to display: ${version}`); + const { useGetTopics } = useTopics(); + const { data, loading } = useGetTopics(); + return (
Strimzi logo

Welcome to the Strimzi UI

{showVersion && isComplete &&

{`Version: ${version}`}

} + {loading || !data ? ( +

Loading...

+ ) : ( +

{`Number of topics: ${data.topics.length}`}

+ )} {children}
); diff --git a/client/Queries/Topics/index.ts b/client/Queries/Topics/index.ts new file mode 100644 index 00000000..82224588 --- /dev/null +++ b/client/Queries/Topics/index.ts @@ -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). + */ +import gql from 'graphql-tag'; + +export const GET_TOPICS = gql` + query { + topics { + name + partitions + replicas + } + } +`;