Selaa lähdekoodia

Add topic messages UI

Maxim Tereshin 5 vuotta sitten
vanhempi
commit
046b84bdb5

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 431 - 255
kafka-ui-react-app/package-lock.json


+ 1 - 0
kafka-ui-react-app/package.json

@@ -19,6 +19,7 @@
     "bulma": "^0.8.0",
     "bulma-switch": "^2.0.0",
     "classnames": "^2.2.6",
+    "date-fns": "^2.14.0",
     "json-server": "^0.15.1",
     "lodash": "^4.17.15",
     "node-sass": "^4.13.1",

+ 1 - 1
kafka-ui-react-app/src/components/App.scss

@@ -25,6 +25,6 @@ $navbar-width: 250px;
     left: 0;
     bottom: 0;
     padding: 20px 20px;
-
+    overflow-y: scroll;
   }
 }

+ 90 - 4
kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx

@@ -1,19 +1,105 @@
 import React from 'react';
-import { ClusterName, TopicName } from 'redux/interfaces';
+import { ClusterName, TopicMessage, TopicName } from 'redux/interfaces';
+import PageLoader from 'components/common/PageLoader/PageLoader';
+import { format } from 'date-fns';
 
 interface Props {
   clusterName: ClusterName;
   topicName: TopicName;
+  isFetched: boolean;
+  fetchTopicMessages: (clusterName: ClusterName, topicName: TopicName) => void;
+  messages: TopicMessage[];
 }
 
 const Messages: React.FC<Props> = ({
+  isFetched,
   clusterName,
   topicName,
+  messages,
+  fetchTopicMessages,
 }) => {
+  React.useEffect(() => {
+    fetchTopicMessages(clusterName, topicName);
+  }, [fetchTopicMessages, clusterName, topicName]);
+
+  const [searchText, setSearchText] = React.useState<string>('');
+
+  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setSearchText(event.target.value);
+  };
+
+  const getTimestampDate = (timestamp: number) => {
+    return format(new Date(timestamp * 1000), 'MM.dd.yyyy HH:mm:ss');
+  };
+
+  const getMessageContentHeaders = () => {
+    const message = messages[0];
+    const headers: JSX.Element[] = [];
+    const content = JSON.parse(message.content);
+    Object.keys(content).forEach((k) =>
+      headers.push(<th>{`content.${k}`}</th>)
+    );
+
+    return headers;
+  };
+
+  const getMessageContentBody = (content: string) => {
+    const c = JSON.parse(content);
+    const columns: JSX.Element[] = [];
+    Object.values(c).map((v) => columns.push(<td>{JSON.stringify(v)}</td>));
+    return columns;
+  };
+
   return (
-    <h1>
-      Messages from {clusterName}{topicName}
-    </h1>
+    // eslint-disable-next-line no-nested-ternary
+    isFetched ? (
+      messages.length > 0 ? (
+        <div>
+          <div className="columns">
+            <div className="column is-half is-offset-half">
+              <input
+                id="searchText"
+                type="text"
+                name="searchText"
+                className="input"
+                placeholder="Search"
+                value={searchText}
+                onChange={handleInputChange}
+              />
+            </div>
+          </div>
+          <table className="table is-striped is-fullwidth">
+            <thead>
+              <tr>
+                <th>Timestamp</th>
+                <th>Offset</th>
+                <th>Partition</th>
+                {getMessageContentHeaders()}
+              </tr>
+            </thead>
+            <tbody>
+              {messages
+                .filter(
+                  (message) =>
+                    !searchText || message?.content?.indexOf(searchText) >= 0
+                )
+                .map((message) => (
+                  <tr key={message.timestamp}>
+                    <td>{getTimestampDate(message.timestamp)}</td>
+                    <td>{message.offset}</td>
+                    <td>{message.partition}</td>
+                    {getMessageContentBody(message.content)}
+                  </tr>
+                ))}
+            </tbody>
+          </table>
+        </div>
+      ) : (
+        <div>No messages at selected topic</div>
+      )
+    ) : (
+      <PageLoader isFullHeight={false} />
+    )
   );
 };
 

+ 25 - 5
kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts

@@ -1,20 +1,40 @@
 import { connect } from 'react-redux';
+import { ClusterName, RootState, TopicName } from 'redux/interfaces';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+import { fetchTopicMessages } from 'redux/actions';
+import {
+  getIsTopicMessagesFetched,
+  getTopicMessages,
+} from 'redux/reducers/topics/selectors';
+
 import Messages from './Messages';
-import {ClusterName, RootState, TopicName} from 'redux/interfaces';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
 
 interface RouteProps {
   clusterName: ClusterName;
   topicName: TopicName;
 }
 
-interface OwnProps extends RouteComponentProps<RouteProps> { }
+type OwnProps = RouteComponentProps<RouteProps>;
 
-const mapStateToProps = (state: RootState, { match: { params: { topicName, clusterName } } }: OwnProps) => ({
+const mapStateToProps = (
+  state: RootState,
+  {
+    match: {
+      params: { topicName, clusterName },
+    },
+  }: OwnProps
+) => ({
   clusterName,
   topicName,
+  isFetched: getIsTopicMessagesFetched(state),
+  messages: getTopicMessages(state),
 });
 
+const mapDispatchToProps = {
+  fetchTopicMessages: (clusterName: ClusterName, topicName: TopicName) =>
+    fetchTopicMessages(clusterName, topicName),
+};
+
 export default withRouter(
-  connect(mapStateToProps)(Messages)
+  connect(mapStateToProps, mapDispatchToProps)(Messages)
 );

+ 16 - 3
kafka-ui-react-app/src/components/common/PageLoader/PageLoader.tsx

@@ -1,8 +1,21 @@
 import React from 'react';
+import cx from 'classnames';
 
-const PageLoader: React.FC = () => (
-  <section className="hero is-fullheight-with-navbar">
-    <div className="hero-body has-text-centered" style={{ justifyContent: 'center' }}>
+interface Props {
+  isFullHeight: boolean;
+}
+
+const PageLoader: React.FC<Partial<Props>> = ({ isFullHeight = true }) => (
+  <section
+    className={cx(
+      'hero',
+      isFullHeight ? 'is-fullheight-with-navbar' : 'is-halfheight'
+    )}
+  >
+    <div
+      className="hero-body has-text-centered"
+      style={{ justifyContent: 'center' }}
+    >
       <div style={{ width: 300 }}>
         <div className="subtitle">Loading...</div>
         <progress

+ 6 - 1
kafka-ui-react-app/src/redux/actionType.ts

@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
 export enum ActionType {
   GET_CLUSTERS__REQUEST = 'GET_CLUSTERS__REQUEST',
   GET_CLUSTERS__SUCCESS = 'GET_CLUSTERS__SUCCESS',
@@ -15,6 +16,10 @@ export enum ActionType {
   GET_TOPICS__SUCCESS = 'GET_TOPICS__SUCCESS',
   GET_TOPICS__FAILURE = 'GET_TOPICS__FAILURE',
 
+  GET_TOPIC_MESSAGES__REQUEST = 'GET_TOPIC_MESSAGES__REQUEST',
+  GET_TOPIC_MESSAGES__SUCCESS = 'GET_TOPIC_MESSAGES__SUCCESS',
+  GET_TOPIC_MESSAGES__FAILURE = 'GET_TOPIC_MESSAGES__FAILURE',
+
   GET_TOPIC_DETAILS__REQUEST = 'GET_TOPIC_DETAILS__REQUEST',
   GET_TOPIC_DETAILS__SUCCESS = 'GET_TOPIC_DETAILS__SUCCESS',
   GET_TOPIC_DETAILS__FAILURE = 'GET_TOPIC_DETAILS__FAILURE',
@@ -34,4 +39,4 @@ export enum ActionType {
   GET_CONSUMER_GROUP_DETAILS__REQUEST = 'GET_CONSUMER_GROUP_DETAILS__REQUEST',
   GET_CONSUMER_GROUP_DETAILS__SUCCESS = 'GET_CONSUMER_GROUP_DETAILS__SUCCESS',
   GET_CONSUMER_GROUP_DETAILS__FAILURE = 'GET_CONSUMER_GROUP_DETAILS__FAILURE',
-};
+}

+ 28 - 13
kafka-ui-react-app/src/redux/actions/actions.ts

@@ -1,6 +1,5 @@
 import { createAsyncAction } from 'typesafe-actions';
 import { ActionType } from 'redux/actionType';
-import { ConsumerGroup, ConsumerGroupID, ConsumerGroupDetails } from '../interfaces/consumerGroup';
 import {
   Broker,
   BrokerMetrics,
@@ -8,59 +7,75 @@ import {
   Topic,
   TopicConfig,
   TopicDetails,
+  TopicMessage,
   TopicName,
 } from 'redux/interfaces';
+import {
+  ConsumerGroup,
+  ConsumerGroupID,
+  ConsumerGroupDetails,
+} from '../interfaces/consumerGroup';
 
 export const fetchBrokersAction = createAsyncAction(
   ActionType.GET_BROKERS__REQUEST,
   ActionType.GET_BROKERS__SUCCESS,
-  ActionType.GET_BROKERS__FAILURE,
+  ActionType.GET_BROKERS__FAILURE
 )<undefined, Broker[], undefined>();
 
 export const fetchBrokerMetricsAction = createAsyncAction(
   ActionType.GET_BROKER_METRICS__REQUEST,
   ActionType.GET_BROKER_METRICS__SUCCESS,
-  ActionType.GET_BROKER_METRICS__FAILURE,
+  ActionType.GET_BROKER_METRICS__FAILURE
 )<undefined, BrokerMetrics, undefined>();
 
 export const fetchClusterListAction = createAsyncAction(
   ActionType.GET_CLUSTERS__REQUEST,
   ActionType.GET_CLUSTERS__SUCCESS,
-  ActionType.GET_CLUSTERS__FAILURE,
+  ActionType.GET_CLUSTERS__FAILURE
 )<undefined, Cluster[], undefined>();
 
 export const fetchTopicListAction = createAsyncAction(
   ActionType.GET_TOPICS__REQUEST,
   ActionType.GET_TOPICS__SUCCESS,
-  ActionType.GET_TOPICS__FAILURE,
+  ActionType.GET_TOPICS__FAILURE
 )<undefined, Topic[], undefined>();
 
+export const fetchTopicMessagesAction = createAsyncAction(
+  ActionType.GET_TOPIC_MESSAGES__REQUEST,
+  ActionType.GET_TOPIC_MESSAGES__SUCCESS,
+  ActionType.GET_TOPIC_MESSAGES__FAILURE
+)<undefined, TopicMessage[], undefined>();
+
 export const fetchTopicDetailsAction = createAsyncAction(
   ActionType.GET_TOPIC_DETAILS__REQUEST,
   ActionType.GET_TOPIC_DETAILS__SUCCESS,
-  ActionType.GET_TOPIC_DETAILS__FAILURE,
-)<undefined, { topicName: TopicName, details: TopicDetails }, undefined>();
+  ActionType.GET_TOPIC_DETAILS__FAILURE
+)<undefined, { topicName: TopicName; details: TopicDetails }, undefined>();
 
 export const fetchTopicConfigAction = createAsyncAction(
   ActionType.GET_TOPIC_CONFIG__REQUEST,
   ActionType.GET_TOPIC_CONFIG__SUCCESS,
-  ActionType.GET_TOPIC_CONFIG__FAILURE,
-)<undefined, { topicName: TopicName, config: TopicConfig[] }, undefined>();
+  ActionType.GET_TOPIC_CONFIG__FAILURE
+)<undefined, { topicName: TopicName; config: TopicConfig[] }, undefined>();
 
 export const createTopicAction = createAsyncAction(
   ActionType.POST_TOPIC__REQUEST,
   ActionType.POST_TOPIC__SUCCESS,
-  ActionType.POST_TOPIC__FAILURE,
+  ActionType.POST_TOPIC__FAILURE
 )<undefined, Topic, undefined>();
 
 export const fetchConsumerGroupsAction = createAsyncAction(
   ActionType.GET_CONSUMER_GROUPS__REQUEST,
   ActionType.GET_CONSUMER_GROUPS__SUCCESS,
-  ActionType.GET_CONSUMER_GROUPS__FAILURE,
+  ActionType.GET_CONSUMER_GROUPS__FAILURE
 )<undefined, ConsumerGroup[], undefined>();
 
 export const fetchConsumerGroupDetailsAction = createAsyncAction(
   ActionType.GET_CONSUMER_GROUP_DETAILS__REQUEST,
   ActionType.GET_CONSUMER_GROUP_DETAILS__SUCCESS,
-  ActionType.GET_CONSUMER_GROUP_DETAILS__FAILURE,
-)<undefined, { consumerGroupID: ConsumerGroupID, details: ConsumerGroupDetails }, undefined>();
+  ActionType.GET_CONSUMER_GROUP_DETAILS__FAILURE
+)<
+  undefined,
+  { consumerGroupID: ConsumerGroupID; details: ConsumerGroupDetails },
+  undefined
+>();

+ 61 - 14
kafka-ui-react-app/src/redux/actions/thunks.ts

@@ -1,15 +1,18 @@
 import * as api from 'redux/api';
-import * as actions from './actions';
-import { ConsumerGroupID } from '../interfaces/consumerGroup';
 import {
   PromiseThunk,
   Cluster,
   ClusterName,
   TopicFormData,
-  TopicName, Topic,
+  TopicName,
+  Topic,
 } from 'redux/interfaces';
+import * as actions from './actions';
+import { ConsumerGroupID } from '../interfaces/consumerGroup';
 
-export const fetchBrokers = (clusterName: ClusterName): PromiseThunk<void> => async (dispatch) => {
+export const fetchBrokers = (
+  clusterName: ClusterName
+): PromiseThunk<void> => async (dispatch) => {
   dispatch(actions.fetchBrokersAction.request());
   try {
     const payload = await api.getBrokers(clusterName);
@@ -19,7 +22,9 @@ export const fetchBrokers = (clusterName: ClusterName): PromiseThunk<void> => as
   }
 };
 
-export const fetchBrokerMetrics = (clusterName: ClusterName): PromiseThunk<void> => async (dispatch) => {
+export const fetchBrokerMetrics = (
+  clusterName: ClusterName
+): PromiseThunk<void> => async (dispatch) => {
   dispatch(actions.fetchBrokerMetricsAction.request());
   try {
     const payload = await api.getBrokerMetrics(clusterName);
@@ -39,7 +44,9 @@ export const fetchClustersList = (): PromiseThunk<void> => async (dispatch) => {
   }
 };
 
-export const fetchTopicList = (clusterName: ClusterName): PromiseThunk<void> => async (dispatch) => {
+export const fetchTopicList = (
+  clusterName: ClusterName
+): PromiseThunk<void> => async (dispatch) => {
   dispatch(actions.fetchTopicListAction.request());
   try {
     const topics = await api.getTopics(clusterName);
@@ -49,17 +56,41 @@ export const fetchTopicList = (clusterName: ClusterName): PromiseThunk<void> =>
   }
 };
 
-export const fetchTopicDetails = (clusterName: ClusterName, topicName: TopicName): PromiseThunk<void> => async (dispatch) => {
+export const fetchTopicMessages = (
+  clusterName: ClusterName,
+  topicName: TopicName
+): PromiseThunk<void> => async (dispatch) => {
+  dispatch(actions.fetchTopicMessagesAction.request());
+  try {
+    const messages = await api.getTopicMessages(clusterName, topicName);
+    dispatch(actions.fetchTopicMessagesAction.success(messages));
+  } catch (e) {
+    dispatch(actions.fetchTopicMessagesAction.failure());
+  }
+};
+
+export const fetchTopicDetails = (
+  clusterName: ClusterName,
+  topicName: TopicName
+): PromiseThunk<void> => async (dispatch) => {
   dispatch(actions.fetchTopicDetailsAction.request());
   try {
     const topicDetails = await api.getTopicDetails(clusterName, topicName);
-    dispatch(actions.fetchTopicDetailsAction.success({ topicName, details: topicDetails }));
+    dispatch(
+      actions.fetchTopicDetailsAction.success({
+        topicName,
+        details: topicDetails,
+      })
+    );
   } catch (e) {
     dispatch(actions.fetchTopicDetailsAction.failure());
   }
 };
 
-export const fetchTopicConfig = (clusterName: ClusterName, topicName: TopicName): PromiseThunk<void> => async (dispatch) => {
+export const fetchTopicConfig = (
+  clusterName: ClusterName,
+  topicName: TopicName
+): PromiseThunk<void> => async (dispatch) => {
   dispatch(actions.fetchTopicConfigAction.request());
   try {
     const config = await api.getTopicConfig(clusterName, topicName);
@@ -69,7 +100,10 @@ export const fetchTopicConfig = (clusterName: ClusterName, topicName: TopicName)
   }
 };
 
-export const createTopic = (clusterName: ClusterName, form: TopicFormData): PromiseThunk<void> => async (dispatch) => {
+export const createTopic = (
+  clusterName: ClusterName,
+  form: TopicFormData
+): PromiseThunk<void> => async (dispatch) => {
   dispatch(actions.createTopicAction.request());
   try {
     const topic: Topic = await api.postTopic(clusterName, form);
@@ -79,7 +113,9 @@ export const createTopic = (clusterName: ClusterName, form: TopicFormData): Prom
   }
 };
 
-export const fetchConsumerGroupsList = (clusterName: ClusterName): PromiseThunk<void> => async (dispatch) => {
+export const fetchConsumerGroupsList = (
+  clusterName: ClusterName
+): PromiseThunk<void> => async (dispatch) => {
   dispatch(actions.fetchConsumerGroupsAction.request());
   try {
     const consumerGroups = await api.getConsumerGroups(clusterName);
@@ -89,11 +125,22 @@ export const fetchConsumerGroupsList = (clusterName: ClusterName): PromiseThunk<
   }
 };
 
-export const fetchConsumerGroupDetails = (clusterName: ClusterName, consumerGroupID: ConsumerGroupID): PromiseThunk<void> => async (dispatch) => {
+export const fetchConsumerGroupDetails = (
+  clusterName: ClusterName,
+  consumerGroupID: ConsumerGroupID
+): PromiseThunk<void> => async (dispatch) => {
   dispatch(actions.fetchConsumerGroupDetailsAction.request());
   try {
-    const consumerGroupDetails = await api.getConsumerGroupDetails(clusterName, consumerGroupID);
-    dispatch(actions.fetchConsumerGroupDetailsAction.success({ consumerGroupID, details: consumerGroupDetails }));
+    const consumerGroupDetails = await api.getConsumerGroupDetails(
+      clusterName,
+      consumerGroupID
+    );
+    dispatch(
+      actions.fetchConsumerGroupDetailsAction.success({
+        consumerGroupID,
+        details: consumerGroupDetails,
+      })
+    );
   } catch (e) {
     dispatch(actions.fetchConsumerGroupDetailsAction.failure());
   }

+ 9 - 0
kafka-ui-react-app/src/redux/api/topics.ts

@@ -6,6 +6,7 @@ import {
   TopicDetails,
   TopicFormCustomParam,
   TopicFormData,
+  TopicMessage,
   TopicName,
 } from 'redux/interfaces';
 import { BASE_PARAMS, BASE_URL } from 'lib/constants';
@@ -31,6 +32,14 @@ export const getTopics = (clusterName: ClusterName): Promise<Topic[]> =>
     ...BASE_PARAMS,
   }).then((res) => res.json());
 
+export const getTopicMessages = (
+  clusterName: ClusterName,
+  topicName: TopicName
+): Promise<TopicMessage[]> =>
+  fetch(`${BASE_URL}/clusters/${clusterName}/topics/${topicName}/messages`, {
+    ...BASE_PARAMS,
+  }).then((res) => res.json());
+
 interface Result {
   [index: string]: string;
 }

+ 11 - 0
kafka-ui-react-app/src/redux/interfaces/topic.ts

@@ -44,6 +44,16 @@ export interface Topic {
   partitions: TopicPartition[];
 }
 
+export interface TopicMessage {
+  partition: number;
+  offset: number;
+  timestamp: number;
+  timestampType: string;
+  key: string;
+  headers: Record<string, string>;
+  content: string;
+}
+
 export interface TopicFormCustomParam {
   name: string;
   value: string;
@@ -61,6 +71,7 @@ export interface TopicWithDetailedInfo extends Topic, TopicDetails {
 export interface TopicsState {
   byName: { [topicName: string]: TopicWithDetailedInfo };
   allNames: TopicName[];
+  messages: TopicMessage[];
 }
 
 export interface TopicFormData {

+ 20 - 17
kafka-ui-react-app/src/redux/reducers/topics/reducer.ts

@@ -4,6 +4,7 @@ import { ActionType } from 'redux/actionType';
 export const initialState: TopicsState = {
   byName: {},
   allNames: [],
+  messages: [],
 };
 
 const updateTopicList = (state: TopicsState, payload: Topic[]): TopicsState => {
@@ -12,24 +13,21 @@ const updateTopicList = (state: TopicsState, payload: Topic[]): TopicsState => {
     allNames: [],
   };
 
-  return payload.reduce(
-    (memo: TopicsState, topic) => {
-      const {name} = topic;
-      memo.byName[name] = {
-        ...memo.byName[name],
-        ...topic,
-      };
-      memo.allNames.push(name);
+  return payload.reduce((memo: TopicsState, topic) => {
+    const { name } = topic;
+    memo.byName[name] = {
+      ...memo.byName[name],
+      ...topic,
+    };
+    memo.allNames.push(name);
 
-      return memo;
-    },
-    initialMemo,
-  );
+    return memo;
+  }, initialMemo);
 };
 
 const addToTopicList = (state: TopicsState, payload: Topic): TopicsState => {
   const newState: TopicsState = {
-    ...state
+    ...state,
   };
   newState.allNames.push(payload.name);
   newState.byName[payload.name] = payload;
@@ -48,8 +46,13 @@ const reducer = (state = initialState, action: Action): TopicsState => {
           [action.payload.topicName]: {
             ...state.byName[action.payload.topicName],
             ...action.payload.details,
-          }
-        }
+          },
+        },
+      };
+    case ActionType.GET_TOPIC_MESSAGES__SUCCESS:
+      return {
+        ...state,
+        messages: action.payload,
       };
     case ActionType.GET_TOPIC_CONFIG__SUCCESS:
       return {
@@ -59,8 +62,8 @@ const reducer = (state = initialState, action: Action): TopicsState => {
           [action.payload.topicName]: {
             ...state.byName[action.payload.topicName],
             config: action.payload.config,
-          }
-        }
+          },
+        },
       };
     case ActionType.POST_TOPIC__SUCCESS:
       return addToTopicList(state, action.payload);

+ 32 - 13
kafka-ui-react-app/src/redux/reducers/topics/selectors.ts

@@ -1,35 +1,52 @@
 import { createSelector } from 'reselect';
-import { RootState, TopicName, FetchStatus, TopicsState } from 'redux/interfaces';
+import {
+  RootState,
+  TopicName,
+  FetchStatus,
+  TopicsState,
+} from 'redux/interfaces';
 import { createFetchingSelector } from 'redux/reducers/loader/selectors';
 
 const topicsState = ({ topics }: RootState): TopicsState => topics;
 
 const getAllNames = (state: RootState) => topicsState(state).allNames;
 const getTopicMap = (state: RootState) => topicsState(state).byName;
+export const getTopicMessages = (state: RootState) =>
+  topicsState(state).messages;
 
 const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
-const getTopicDetailsFetchingStatus = createFetchingSelector('GET_TOPIC_DETAILS');
+const getTopicDetailsFetchingStatus = createFetchingSelector(
+  'GET_TOPIC_DETAILS'
+);
+const getTopicMessagesFetchingStatus = createFetchingSelector(
+  'GET_TOPIC_MESSAGES'
+);
 const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
 const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
 
 export const getIsTopicListFetched = createSelector(
   getTopicListFetchingStatus,
-  (status) => status === FetchStatus.fetched,
+  (status) => status === FetchStatus.fetched
 );
 
 export const getIsTopicDetailsFetched = createSelector(
   getTopicDetailsFetchingStatus,
-  (status) => status === FetchStatus.fetched,
+  (status) => status === FetchStatus.fetched
+);
+
+export const getIsTopicMessagesFetched = createSelector(
+  getTopicMessagesFetchingStatus,
+  (status) => status === FetchStatus.fetched
 );
 
 export const getTopicConfigFetched = createSelector(
   getTopicConfigFetchingStatus,
-  (status) => status === FetchStatus.fetched,
+  (status) => status === FetchStatus.fetched
 );
 
 export const getTopicCreated = createSelector(
   getTopicCreationStatus,
-  (status) => status === FetchStatus.fetched,
+  (status) => status === FetchStatus.fetched
 );
 
 export const getTopicList = createSelector(
@@ -40,13 +57,12 @@ export const getTopicList = createSelector(
     if (!isFetched) {
       return [];
     }
-    return allNames.map((name) => byName[name])
-  },
+    return allNames.map((name) => byName[name]);
+  }
 );
 
-export const getExternalTopicList = createSelector(
-  getTopicList,
-  (topics) => topics.filter(({ internal }) => !internal),
+export const getExternalTopicList = createSelector(getTopicList, (topics) =>
+  topics.filter(({ internal }) => !internal)
 );
 
 const getTopicName = (_: RootState, topicName: TopicName) => topicName;
@@ -54,7 +70,10 @@ const getTopicName = (_: RootState, topicName: TopicName) => topicName;
 export const getTopicByName = createSelector(
   getTopicMap,
   getTopicName,
-  (topics, topicName) => topics[topicName],
+  (topics, topicName) => topics[topicName]
 );
 
-export const getTopicConfig = createSelector(getTopicByName, ({ config }) => config);
+export const getTopicConfig = createSelector(
+  getTopicByName,
+  ({ config }) => config
+);

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä