From 8a6d8195dd783ac9b104a7f9a96014469adda182 Mon Sep 17 00:00:00 2001 From: Aitor Algorta Date: Thu, 11 Jan 2024 10:44:52 +0100 Subject: [PATCH] [#4137] Improve Kafka Sections (#4138) * add section * improvement of schemas enrichment * some fixes * some fixes * some fixes --- frontend/control-center/src/App.tsx | 6 + .../src/actions/streams/index.ts | 25 +- .../src/components/Sidebar/index.tsx | 48 ++- .../ConnectorWrapper/index.module.scss | 3 + .../LLMConsumers/EmptyState/index.module.scss | 52 +++ .../pages/LLMConsumers/EmptyState/index.tsx | 30 ++ .../LLMConsumerItem/index.module.scss | 47 +++ .../LLMConsumers/LLMConsumerItem/index.tsx | 38 +++ .../src/pages/LLMConsumers/index.module.scss | 136 ++++++++ .../src/pages/LLMConsumers/index.tsx | 216 +++++++++++++ .../pages/LLMs/EmptyState/index.module.scss | 52 +++ .../src/pages/LLMs/EmptyState/index.tsx | 30 ++ .../pages/LLMs/LLMInfoItem/index.module.scss | 21 ++ .../src/pages/LLMs/LLMInfoItem/index.tsx | 18 ++ .../src/pages/LLMs/index.module.scss | 106 +++++++ .../control-center/src/pages/LLMs/index.tsx | 92 ++++++ .../EnrichedSchemaSection/index.tsx | 296 ++++++++++++++++++ .../SchemaDescription/SchemaDescription.tsx | 45 ++- .../SchemaDescription/SchemaSection/index.tsx | 97 +++++- .../SchemaDescription/index.module.scss | 67 ++++ .../src/pages/Schemas/SchemaItem/index.tsx | 7 + .../TopicDescription/TopicDescription.tsx | 13 +- .../src/pages/Topics/index.module.scss | 2 +- .../src/reducers/data/streams/index.ts | 11 + frontend/control-center/src/routes/routes.ts | 4 + .../control-center/src/services/format.ts | 2 +- .../SettingsModal/ModalHeader.module.scss | 2 +- lib/typescript/httpclient/src/client.ts | 28 ++ .../httpclient/src/endpoints/index.ts | 6 + .../src/endpoints/llmConsumersCreate.ts | 11 + .../src/endpoints/llmConsumersDelete.ts | 6 + .../src/endpoints/llmConsumersList.ts | 6 + .../httpclient/src/endpoints/llmInfo.ts | 6 + .../httpclient/src/endpoints/llmQuery.ts | 6 + .../httpclient/src/endpoints/llmStats.ts | 6 + .../LLMConsumerCreateRequestPayload.ts | 6 + .../LLMConsumerCreateResponsePayload.ts | 3 + .../src/payload/LLMConsumerListPayload.ts | 10 + .../src/payload/LLMConsumersDeletePayload.ts | 3 + .../httpclient/src/payload/LLMInfoPayload.ts | 8 + .../src/payload/LLMQueryRequestPayload.ts | 3 + .../src/payload/LLMQueryResponsePayload.ts | 6 + .../httpclient/src/payload/LLMStatsPayload.ts | 3 + .../httpclient/src/payload/index.ts | 8 + lib/typescript/model/Streams.ts | 3 + lib/typescript/translations/translations.ts | 40 +++ 46 files changed, 1586 insertions(+), 48 deletions(-) create mode 100644 frontend/control-center/src/pages/LLMConsumers/EmptyState/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMConsumers/EmptyState/index.tsx create mode 100644 frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.tsx create mode 100644 frontend/control-center/src/pages/LLMConsumers/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMConsumers/index.tsx create mode 100644 frontend/control-center/src/pages/LLMs/EmptyState/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMs/EmptyState/index.tsx create mode 100644 frontend/control-center/src/pages/LLMs/LLMInfoItem/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMs/LLMInfoItem/index.tsx create mode 100644 frontend/control-center/src/pages/LLMs/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMs/index.tsx create mode 100644 frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/EnrichedSchemaSection/index.tsx create mode 100644 lib/typescript/httpclient/src/endpoints/llmConsumersCreate.ts create mode 100644 lib/typescript/httpclient/src/endpoints/llmConsumersDelete.ts create mode 100644 lib/typescript/httpclient/src/endpoints/llmConsumersList.ts create mode 100644 lib/typescript/httpclient/src/endpoints/llmInfo.ts create mode 100644 lib/typescript/httpclient/src/endpoints/llmQuery.ts create mode 100644 lib/typescript/httpclient/src/endpoints/llmStats.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMConsumerCreateRequestPayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMConsumerCreateResponsePayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMConsumerListPayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMConsumersDeletePayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMInfoPayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMQueryRequestPayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMQueryResponsePayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMStatsPayload.ts diff --git a/frontend/control-center/src/App.tsx b/frontend/control-center/src/App.tsx index 294aabaae9..4788b33dbb 100644 --- a/frontend/control-center/src/App.tsx +++ b/frontend/control-center/src/App.tsx @@ -17,6 +17,8 @@ import { STREAMS_ROUTE, TOPICS_ROUTE, SCHEMAS_ROUTE, + LLMS_ROUTE, + LLM_CONSUMERS_ROUTE, } from './routes/routes'; import NotFound from './pages/NotFound'; import ConnectorsOutlet from './pages/Connectors/ConnectorsOutlet'; @@ -36,6 +38,8 @@ import {getAppExternalURL} from './services/getAppExternalURL'; import Streams from './pages/Streams'; import Topics from './pages/Topics'; import Schemas from './pages/Schemas'; +import LLMs from './pages/LLMs'; +import LLMConsumers from './pages/LLMConsumers'; const mapDispatchToProps = { getClientConfig, @@ -111,6 +115,8 @@ const App = (props: ConnectedProps) => { } /> } /> + } /> + } /> } /> diff --git a/frontend/control-center/src/actions/streams/index.ts b/frontend/control-center/src/actions/streams/index.ts index 3d47b61e03..26619e2e6c 100644 --- a/frontend/control-center/src/actions/streams/index.ts +++ b/frontend/control-center/src/actions/streams/index.ts @@ -10,6 +10,7 @@ const SET_TOPIC_INFO = '@@metadata/SET_TOPIC_INFO'; const SET_TOPIC_SCHEMAS = '@@metadata/SET_TOPIC_SCHEMAS'; const SET_STREAMS = '@@metadata/SET_STREAMS'; const SET_SCHEMAS_INFO = '@@metadata/SET_SCHEMAS_INFO'; +const SET_SCHEMAS_VERSIONS = '@@metadata/SET_SCHEMAS_VERSIONS'; const SET_STREAM_INFO = '@@metadata/SET_STREAM_INFO'; const SET_LAST_MESSAGE = '@@metadata/SET_LAST_MESSAGRE'; @@ -74,8 +75,23 @@ export const getSchemas = () => async (dispatch: Dispatch) => { }); }; -export const getSchemaInfo = (topicName: string) => async (dispatch: Dispatch) => { - return getData(`subjects/${topicName}/versions/latest`).then(response => { +export const getSchemaVersions = (topicName: string) => async (dispatch: Dispatch) => { + return getData(`subjects/${topicName}/versions`).then(response => { + if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { + return Promise.reject('404 Not Found'); + } else { + dispatch(setCurrentSchemaVersionsAction({name: topicName, versions: response})); + } + return Promise.resolve(true); + }); +}; + +export const getSchemaInfo = (topicName: string, version?: string) => async (dispatch: Dispatch) => { + let v = 'latest'; + if (version) { + v = version; + } + return getData(`subjects/${topicName}/versions/${v}`).then(response => { if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { return Promise.reject('404 Not Found'); } else { @@ -197,6 +213,11 @@ export const setStreamsAction = createAction(SET_STREAMS, (streams: Stream[]) => export const setCurrentSchemaInfoAction = createAction(SET_SCHEMAS_INFO, (topicInfo: Schema) => topicInfo)(); +export const setCurrentSchemaVersionsAction = createAction( + SET_SCHEMAS_VERSIONS, + (topicInfo: {name: string; versions: []}) => topicInfo +)<{name: string; versions: []}>(); + export const setCurrentStreamInfoAction = createAction( SET_STREAM_INFO, (streamInfo: StreamInfo) => streamInfo diff --git a/frontend/control-center/src/components/Sidebar/index.tsx b/frontend/control-center/src/components/Sidebar/index.tsx index 043513f45f..95285c91e9 100644 --- a/frontend/control-center/src/components/Sidebar/index.tsx +++ b/frontend/control-center/src/components/Sidebar/index.tsx @@ -15,6 +15,8 @@ import { STREAMS_ROUTE, TOPICS_ROUTE, SCHEMAS_ROUTE, + LLMS_ROUTE, + LLM_CONSUMERS_ROUTE, } from '../../routes/routes'; import {ReactComponent as ConnectorsIcon} from 'assets/images/icons/gitMerge.svg'; @@ -31,18 +33,18 @@ type SideBarProps = {} & ConnectedProps; const mapStateToProps = (state: StateModel) => ({ version: state.data.config.clusterVersion, components: state.data.config.components, + connectors: state.data.catalog, }); const connector = connect(mapStateToProps); const Sidebar = (props: SideBarProps) => { - const {version, components} = props; + const {version, components, connectors} = props; const componentInfo = useCurrentComponentForSource(Source.airyWebhooks); - const webhooksEnabled = componentInfo.installationStatus === InstallationStatus.installed; const inboxEnabled = components[Source.frontendInbox]?.enabled || false; - const showLine = inboxEnabled || webhooksEnabled; - + const llmsEnabled = connectors['llm-controller']?.installationStatus === 'installed' || false; + const showLine = inboxEnabled || webhooksEnabled || llmsEnabled; const isActive = (route: string) => { return useMatch(`${route}/*`); }; @@ -51,6 +53,9 @@ const Sidebar = (props: SideBarProps) => { const [kafkaSectionOpen, setKafkaSectionOpen] = useState( href.includes(TOPICS_ROUTE) || href.includes(STREAMS_ROUTE) ); + const [llmSectionOpen, setLlmSectionOpen] = useState( + href.includes(LLMS_ROUTE) || href.includes(LLM_CONSUMERS_ROUTE) + ); return ( diff --git a/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.module.scss b/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.module.scss index ae262da930..91129c4325 100644 --- a/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.module.scss +++ b/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.module.scss @@ -114,9 +114,12 @@ .connectorIcon { display: flex; width: 75px; + height: 75px; margin-right: 15px; svg { + width: 75px; + height: 75px; fill: var(--color-text-contrast); } } diff --git a/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.module.scss b/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.module.scss new file mode 100644 index 0000000000..39684b4f2b --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.module.scss @@ -0,0 +1,52 @@ +@import 'assets/scss/colors.scss'; +@import 'assets/scss/fonts.scss'; + +.container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: calc(100% - 88px); +} + +.contentContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + h1 { + @include font-m; + font-weight: 800; + color: var(--color-text-contrast); + margin: 31px 0; + } + + span { + @include font-base; + color: var(--color-text-gray); + } + + .subscribeButton { + color: var(--color-airy-blue); + &:hover { + cursor: pointer; + text-decoration: underline; + } + } +} + +.iconContainer { + display: flex; + justify-content: center; + align-items: center; + background: var(--color-background-gray); + height: 95px; + width: 105px; +} + +.searchIcon { + height: 45px; + width: 45px; + color: var(--color-airy-blue); +} diff --git a/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.tsx b/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.tsx new file mode 100644 index 0000000000..1a542500b8 --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.tsx @@ -0,0 +1,30 @@ +import React, {Dispatch, SetStateAction} from 'react'; +import styles from './index.module.scss'; +import {ReactComponent as SearchIcon} from 'assets/images/icons/search.svg'; +import {useTranslation} from 'react-i18next'; + +type EmptyStateProps = { + createNewLLM: Dispatch>; +}; + +export const EmptyState = (props: EmptyStateProps) => { + const {createNewLLM} = props; + const {t} = useTranslation(); + + return ( +
+
+
+ +
+

{t('noLLMConsumers')}

+ + {t('noLLMConsumersText')} + createNewLLM(true)} className={styles.subscribeButton}> + {t('create') + ' one'} + + +
+
+ ); +}; diff --git a/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.module.scss b/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.module.scss new file mode 100644 index 0000000000..b4f9b0d97a --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.module.scss @@ -0,0 +1,47 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.container { + display: flex; + flex-direction: row; + height: 50px; + align-items: center; + justify-content: flex-start; + + p { + @include font-base; + color: var(--color-text-contrast); + font-weight: bold; + width: 25%; + } + + p:first-child { + width: 30%; + } + + p:nth-child(4) { + width: 15%; + } +} + +.actionButton { + width: 2%; + outline: none; + cursor: pointer; + border: none; + background: none; + padding: 0; +} + +.actionSVG { + width: 16px; + height: 18px; + path { + fill: var(--color-dark-elements-gray); + } + &:hover { + path { + fill: var(--color-airy-blue); + } + } +} diff --git a/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.tsx b/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.tsx new file mode 100644 index 0000000000..b59ce205bc --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import {ReactComponent as TrashIcon} from 'assets/images/icons/trash.svg'; +import {useTranslation} from 'react-i18next'; +import {HttpClientInstance} from '../../../httpClient'; +import styles from './index.module.scss'; +import {NotificationModel} from 'model'; + +type EmptyStateProps = { + item: {name: string; topic: string; status: string; lag: number}; + setNotification: (object: NotificationModel) => void; +}; + +export const LLMConsumerItem = (props: EmptyStateProps) => { + const {item, setNotification} = props; + const {t} = useTranslation(); + + const deleteConsumer = () => { + HttpClientInstance.deleteLLMConsumer({name: item.name}) + .then(() => { + setNotification({show: true, successful: true, text: 'Consumer Deleted'}); + }) + .catch(() => { + setNotification({show: true, successful: false, text: t('errorOccurred')}); + }); + }; + + return ( +
+

{item.name}

+

{item.topic}

+

{item.status}

+

{item.lag}

+ +
+ ); +}; diff --git a/frontend/control-center/src/pages/LLMConsumers/index.module.scss b/frontend/control-center/src/pages/LLMConsumers/index.module.scss new file mode 100644 index 0000000000..8eb971f0db --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/index.module.scss @@ -0,0 +1,136 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; +@import 'assets/scss/animations.scss'; + +.llmsWrapper { + background: var(--color-background-white); + border-top-right-radius: 10px; + border-top-left-radius: 10px; + padding: 32px; + margin: 88px 1.5em 0 191px; + height: calc(100vh - 88px); + overflow-y: scroll; + overflow-x: hidden; + width: 100%; +} + +.headlineContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +} + +.llmsHeadline { + @include font-xl; + font-weight: 900; + letter-spacing: 0; + display: flex; + justify-content: space-between; + color: var(--color-text-contrast); + margin-bottom: 14px; +} + +.llmsHeadlineText { + @include font-xl; + font-weight: 900; +} + +.wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.listHeader { + display: flex; + flex-direction: row; + height: 50px; + align-items: center; + justify-content: flex-start; + + h2 { + @include font-base; + color: var(--color-text-gray); + font-weight: bold; + width: 25%; + } + + h2:first-child { + width: 30%; + } + + h2:last-child { + width: 10%; + } +} + +.successfullySubscribed { + @include font-base; + color: white; +} + +@keyframes translateYIn { + 0% { + transform: translateY(-50px); + opacity: 0; + } + + 50% { + transform: translateY(16px); + opacity: 1; + } + + 100% { + transform: translateY(-50px); + opacity: 0; + } +} + +.translateYAnimIn { + animation: translateYIn 4s ease-in-out; +} + +.animateIn { + animation: fadeInTranslateXLeft 3000ms ease; +} + +.animateOut { + animation: fadeInTranslateXLeft 3000ms ease; +} + +.llmCreateContainer { + display: flex; + flex-direction: column; + justify-content: space-between; + width: 100%; + margin-top: 32px; + button { + @include font-base; + font-weight: bold; + align-self: center; + color: white; + border-radius: 5px; + border: none; + padding: 8px 16px; + margin-top: 16px; + width: 60%; + cursor: pointer; + transition: all 0.2s ease-in-out; + &:hover { + background: var(--color-background-blue); + color: var(--color-text-contrast); + } + } + label { + margin-bottom: 12px; + } +} + +.dropdownContainer { + button { + border: 1px solid gray; + width: 100%; + color: black; + } +} diff --git a/frontend/control-center/src/pages/LLMConsumers/index.tsx b/frontend/control-center/src/pages/LLMConsumers/index.tsx new file mode 100644 index 0000000000..d4d86eed5d --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/index.tsx @@ -0,0 +1,216 @@ +import React, {useEffect, useState} from 'react'; +import {Dropdown, Input, NotificationComponent} from 'components'; +import {SettingsModal} from 'components/alerts/SettingsModal'; +import {Button} from 'components/cta/Button'; +import {useTranslation} from 'react-i18next'; +import {connect, ConnectedProps} from 'react-redux'; +import {setPageTitle} from '../../services/pageTitle'; +import {NotificationModel} from 'model'; +import {AiryLoader} from 'components/loaders/AiryLoader'; +import {EmptyState} from './EmptyState'; +import {HttpClientInstance} from '../../httpClient'; +import {LLMConsumerItem} from './LLMConsumerItem'; +import {getValidTopics} from '../../selectors'; +import {StateModel} from '../../reducers'; +import styles from './index.module.scss'; +import {getSchemaInfo, getSchemas} from '../../actions'; + +type LLMConsumersProps = {} & ConnectedProps; + +const mapDispatchToProps = { + getSchemas, + getSchemaInfo, +}; + +const mapStateToProps = (state: StateModel) => { + return { + topics: getValidTopics(state), + schemas: state.data.streams.schemas, + }; +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +const LLMConsumers = (props: LLMConsumersProps) => { + const {topics, getSchemas} = props; + + const [consumers, setConsumers] = useState([]); + const [notification, setNotification] = useState(null); + const [dataFetched, setDataFetched] = useState(false); + const [showSettingsModal, setShowSettingsModal] = useState(false); + const [name, setName] = useState(''); + const [topic, setTopic] = useState(''); + const [type, setType] = useState(''); + const [textfield, setTextfield] = useState(''); + const [metadataFields, setMetadataFields] = useState(''); + const {t} = useTranslation(); + + useEffect(() => { + setPageTitle('LLM Consumers'); + getSchemas(); + }, []); + + useEffect(() => { + HttpClientInstance.listLLMConsumers() + .then((response: any) => { + setConsumers(response); + setDataFetched(true); + }) + .catch(() => { + handleNotification(true); + }); + }, []); + + const handleNotification = (show: boolean) => { + setNotification({show: show, successful: false, text: t('errorOccurred')}); + }; + + const toggleCreateView = () => { + setShowSettingsModal(!showSettingsModal); + }; + + const createNewLLM = () => { + const metadataFieldsArray = metadataFields.replace(' ', '').split(','); + HttpClientInstance.createLLMConsumer({ + name: name.trim(), + topic: topic.trim(), + textField: textfield.trim(), + metadataFields: metadataFieldsArray, + }) + .then(() => { + setNotification({show: true, successful: true, text: t('llmConsumerCreatedSuccessfully')}); + toggleCreateView(); + setName(''); + setTopic(''); + setTextfield(''); + setMetadataFields(''); + setType(''); + }) + .catch(() => { + handleNotification(true); + }); + }; + + return ( + <> + {' '} + {showSettingsModal && ( + +
+ ) => setName(event.target.value)} + minLength={6} + required={true} + height={32} + fontClass="font-base" + /> +
+ { + setTopic(topic); + // getSchemaInfo(topic).catch(() => { + // getSchemaInfo(topic + '-value'); + // }); + }} + /> +
+ ) => setType(event.target.value)} + minLength={2} + required={true} + height={32} + fontClass="font-base" + /> + ) => setTextfield(event.target.value)} + minLength={2} + required={true} + height={32} + fontClass="font-base" + /> + ) => setMetadataFields(event.target.value)} + minLength={6} + required={true} + height={32} + fontClass="font-base" + /> + +
+
+ )} +
+
+
+

LLM Consumers

+
+
+ +
+
+ {consumers?.length === 0 && dataFetched ? ( + + ) : consumers?.length === 0 ? ( + toggleCreateView()} /> + ) : ( + <> +
+

Name

+

Topic

+

Status

+

Lag

+
+
+ {consumers && + consumers.map((consumer: any) => ( + + ))} +
+ {notification?.show && ( + + )} + + )} +
+ + ); +}; + +export default connector(LLMConsumers); diff --git a/frontend/control-center/src/pages/LLMs/EmptyState/index.module.scss b/frontend/control-center/src/pages/LLMs/EmptyState/index.module.scss new file mode 100644 index 0000000000..39684b4f2b --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/EmptyState/index.module.scss @@ -0,0 +1,52 @@ +@import 'assets/scss/colors.scss'; +@import 'assets/scss/fonts.scss'; + +.container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: calc(100% - 88px); +} + +.contentContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + h1 { + @include font-m; + font-weight: 800; + color: var(--color-text-contrast); + margin: 31px 0; + } + + span { + @include font-base; + color: var(--color-text-gray); + } + + .subscribeButton { + color: var(--color-airy-blue); + &:hover { + cursor: pointer; + text-decoration: underline; + } + } +} + +.iconContainer { + display: flex; + justify-content: center; + align-items: center; + background: var(--color-background-gray); + height: 95px; + width: 105px; +} + +.searchIcon { + height: 45px; + width: 45px; + color: var(--color-airy-blue); +} diff --git a/frontend/control-center/src/pages/LLMs/EmptyState/index.tsx b/frontend/control-center/src/pages/LLMs/EmptyState/index.tsx new file mode 100644 index 0000000000..2f1f7d7373 --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/EmptyState/index.tsx @@ -0,0 +1,30 @@ +import React, {Dispatch, SetStateAction} from 'react'; +import styles from './index.module.scss'; +import {ReactComponent as SearchIcon} from 'assets/images/icons/search.svg'; +import {useTranslation} from 'react-i18next'; + +type EmptyStateProps = { + createNewLLM: Dispatch>; +}; + +export const EmptyState = (props: EmptyStateProps) => { + const {createNewLLM} = props; + const {t} = useTranslation(); + + return ( +
+
+
+ +
+

{t('noLLMs')}

+ + {t('noLLMsText')} + createNewLLM(true)} className={styles.subscribeButton}> + {t('create') + ' one'} + + +
+
+ ); +}; diff --git a/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.module.scss b/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.module.scss new file mode 100644 index 0000000000..331d24a20e --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.module.scss @@ -0,0 +1,21 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.container { + display: flex; + flex-direction: row; + height: 50px; + align-items: center; + justify-content: flex-start; + + p { + @include font-base; + color: var(--color-text-contrast); + font-weight: bold; + width: 25%; + } + + p:first-child { + width: 30%; + } +} diff --git a/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.tsx b/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.tsx new file mode 100644 index 0000000000..bcea927a86 --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import styles from './index.module.scss'; + +type EmptyStateProps = { + item: {llm: string; vectorDatabase: string; llmModel: string}; +}; + +export const LLMInfoItem = (props: EmptyStateProps) => { + const {item} = props; + + return ( +
+

{item.llm}

+

{item.vectorDatabase}

+

{item.llmModel}

+
+ ); +}; diff --git a/frontend/control-center/src/pages/LLMs/index.module.scss b/frontend/control-center/src/pages/LLMs/index.module.scss new file mode 100644 index 0000000000..31195db2da --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/index.module.scss @@ -0,0 +1,106 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; +@import 'assets/scss/animations.scss'; + +.webhooksWrapper { + background: var(--color-background-white); + border-top-right-radius: 10px; + border-top-left-radius: 10px; + padding: 32px; + margin: 88px 1.5em 0 191px; + height: calc(100vh - 88px); + overflow-y: scroll; + overflow-x: hidden; + width: 100%; +} + +.headlineContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +} + +.webhooksHeadline { + @include font-xl; + font-weight: 900; + letter-spacing: 0; + display: flex; + justify-content: space-between; + color: var(--color-text-contrast); + margin-bottom: 14px; +} + +.webhooksHeadlineText { + @include font-xl; + font-weight: 900; +} + +.wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.listHeader { + display: flex; + flex-direction: row; + height: 50px; + align-items: center; + justify-content: flex-start; + + h2 { + @include font-base; + color: var(--color-text-gray); + font-weight: bold; + width: 25%; + } + + h2:first-child { + width: 30%; + } + + h2:last-child { + width: 10%; + } +} + +.successfullySubscribed { + @include font-base; + color: white; +} + +@keyframes translateYIn { + 0% { + transform: translateY(-50px); + opacity: 0; + } + + 50% { + transform: translateY(16px); + opacity: 1; + } + + 100% { + transform: translateY(-50px); + opacity: 0; + } +} + +.translateYAnimIn { + animation: translateYIn 4s ease-in-out; +} + +.animateIn { + animation: fadeInTranslateXLeft 3000ms ease; +} + +.animateOut { + animation: fadeInTranslateXLeft 3000ms ease; +} + +.embeddingsSection { + color: var(--color-text-contrast); + font-weight: bold; + margin-top: 64px; +} diff --git a/frontend/control-center/src/pages/LLMs/index.tsx b/frontend/control-center/src/pages/LLMs/index.tsx new file mode 100644 index 0000000000..385cdd1cb4 --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/index.tsx @@ -0,0 +1,92 @@ +import React, {useEffect, useState} from 'react'; +import {NotificationComponent} from 'components'; +import {useTranslation} from 'react-i18next'; +import {connect} from 'react-redux'; +import {setPageTitle} from '../../services/pageTitle'; +import {NotificationModel} from 'model'; +import {AiryLoader} from 'components/loaders/AiryLoader'; +import styles from './index.module.scss'; +import {EmptyState} from './EmptyState'; +import {HttpClientInstance} from '../../httpClient'; +import {LLMSStatsPayload} from 'httpclient/src'; +import {LLMInfoItem} from './LLMInfoItem'; + +const mapDispatchToProps = {}; + +const connector = connect(null, mapDispatchToProps); + +const LLMs = () => { + const [llms, setLlms] = useState([]); + const [embeddings, setEmbeddings] = useState(0); + const [notification, setNotification] = useState(null); + const [dataFetched, setDataFetched] = useState(false); + const {t} = useTranslation(); + + useEffect(() => { + setPageTitle('LLMs'); + }, []); + + useEffect(() => { + HttpClientInstance.getLLMInfo() + .then((response: any) => { + setLlms(response); + setDataFetched(true); + }) + .catch(() => { + handleNotification(true); + }); + HttpClientInstance.getLLMStats() + .then((response: LLMSStatsPayload) => { + setEmbeddings(response.embeddings); + }) + .catch(() => { + handleNotification(true); + }); + }, []); + + const handleNotification = (show: boolean) => { + setNotification({show: show, successful: false, text: t('errorOccurred')}); + }; + + const createNewLLM = () => { + console.log('create new LLM'); + }; + + return ( + <> +
+
+
+

LLM Controller

+
+
+ {llms?.length === 0 && dataFetched ? ( + + ) : llms?.length === 0 ? ( + createNewLLM()} /> + ) : ( + <> +
+

LLM Provider

+

Vector Database

+

Model

+
+
{llms && llms.map((llm: any) => )}
+
Embeddings: {embeddings}
+ {notification?.show && ( + + )} + + )} +
+ + ); +}; + +export default connector(LLMs); diff --git a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/EnrichedSchemaSection/index.tsx b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/EnrichedSchemaSection/index.tsx new file mode 100644 index 0000000000..7811f24b5c --- /dev/null +++ b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/EnrichedSchemaSection/index.tsx @@ -0,0 +1,296 @@ +import React, {MutableRefObject, useEffect, useRef, useState} from 'react'; +import MonacoEditor from '@uiw/react-monacoeditor'; +import {calculateHeightOfCodeString, isJSON} from '../../../../../services'; +import {HttpClientInstance} from '../../../../../httpClient'; +import styles from '../index.module.scss'; +import {Button} from 'components'; +import {ConnectedProps, connect} from 'react-redux'; +import {checkCompatibilityOfNewSchema, setSchemaSchema} from '../../../../../actions'; +import {useTranslation} from 'react-i18next'; + +type EnrichedSchemaSectionProps = { + schemaName: string; + code: string; + setCode: (code: string) => void; + setFirstTabSelected: (flag: boolean) => void; + editorMode: string; + wrapperSection: MutableRefObject; + isEditMode: boolean; + setIsEditMode: (flag: boolean) => void; + setErrorMessage: (error: string) => void; + setShowErrorPopUp: (flag: boolean) => void; + version: number; +} & ConnectedProps; + +const mapDispatchToProps = { + setSchemaSchema, + checkCompatibilityOfNewSchema, +}; + +const connector = connect(null, mapDispatchToProps); + +const EnrichedSchemaSection = (props: EnrichedSchemaSectionProps) => { + const { + schemaName, + code, + setCode, + setFirstTabSelected, + editorMode, + wrapperSection, + setSchemaSchema, + isEditMode, + setIsEditMode, + checkCompatibilityOfNewSchema, + setErrorMessage, + setShowErrorPopUp, + version, + } = props; + + const [localCode, setLocalCode] = useState(undefined); + const [hasBeenChanged, setHasBeenChanged] = useState(false); + const codeRef = useRef(null); + const {t} = useTranslation(); + + useEffect(() => { + if (isEnrichmentAvailable(code)) { + setTimeout(() => { + const enriched = localStorage.getItem(schemaName); + if (enriched) { + setLocalCode(enriched); + recalculateContainerHeight(enriched); + } + }, 100); + enrichCode(code); + } else { + wrapperSection.current.style.height = '156px'; + } + }, [code]); + + const resetCodeAndEndEdition = () => { + setIsEditMode(false); + setFirstTabSelected(true); + }; + + const recalculateContainerHeight = (code: string) => { + const basicHeight = 220; + if (wrapperSection && wrapperSection.current) { + wrapperSection.current.style.height = `${calculateHeightOfCodeString(code) + basicHeight}px`; + } else { + wrapperSection.current.style.height = `${basicHeight}px`; + } + if ((wrapperSection.current.style.height.replace('px', '') as number) > 700) { + wrapperSection.current.style.height = '700px'; + } + }; + + const recalculateCodeHeight = (code: string) => { + const codeHeight = calculateHeightOfCodeString(code); + if (codeHeight > 478) { + return 478; + } + return codeHeight; + }; + + const isEnrichmentAvailable = (code: string): boolean => { + let needsEnrichment = false; + const parsedCode = JSON.parse(code); + (parsedCode.fields || []).map(field => { + if (typeof field.type === 'object' && !Array.isArray(field.type)) { + if (!field.type.doc) { + needsEnrichment = true; + } + } else if (!field.doc) { + needsEnrichment = true; + } + }); + return needsEnrichment; + }; + + const enrichCode = async (code: string) => { + let enrichedSchema = localStorage.getItem(schemaName); + + if (!enrichedSchema) { + const enrichedCode = JSON.parse(code); + + // Use map to create an array of promises + const promises = (enrichedCode.fields || []).map(async field => { + console.log(typeof field.type); + if (typeof field.type === 'object' && !Array.isArray(field.type)) { + if (!field.type.doc) { + const doc = await generateDocForField(field); + field.type.doc = doc; + } + } else if (!field.doc) { + const doc = await generateDocForField(field); + field.doc = doc; + } + }); + + // Wait for all promises to resolve + await Promise.all(promises); + + enrichedSchema = JSON.stringify(enrichedCode, null, 2); + localStorage.setItem(schemaName, enrichedSchema); + } + + setLocalCode(enrichedSchema); + recalculateContainerHeight(enrichedSchema); + }; + + const saveEnrichedSchema = () => { + setSchemaSchema(schemaName, JSON.stringify(localCode, null, 2)); + }; + + const checkCompatibility = (_schemaName: string, _code: string, _version: number) => { + checkCompatibilityOfNewSchema(_schemaName, _code, _version) + .then(() => { + setSchemaSchema(_schemaName, _code) + .then(() => { + setCode(localCode); + setHasBeenChanged(false); + }) + .catch((e: string) => { + setIsEditMode(true); + setErrorMessage(e); + setShowErrorPopUp(true); + setTimeout(() => setShowErrorPopUp(false), 5000); + }); + }) + .catch((e: string) => { + if (e.includes('404')) { + checkCompatibility(_schemaName + '-value', _code, _version); + } else { + setIsEditMode(true); + setErrorMessage(e); + setShowErrorPopUp(true); + setTimeout(() => setShowErrorPopUp(false), 5000); + } + }); + }; + + const generateDocForField = async (field: any): Promise => { + try { + const response = await HttpClientInstance.llmQuery({ + query: `This is the payload of a metadata field of a Kafka Schema ${JSON.stringify( + field + )}. A the name of the schema is ${schemaName}. This is the whole schema: ${code}. Give an accurante description of the field, so the users can understand what it is and what it is used for.`, + }); + return response.answer.result; + } catch (error) { + console.error('Error in generateDocForField:', error); + return ''; + } + }; + + return ( + <> +
+
+ + +
+
+ {isEnrichmentAvailable(code) ? ( + <> +
+
+
+ This schema can be automatically enriched with documentation and saved as a new version as follows. +
+ +
+
+
New schema:
+
+ + {hasBeenChanged && ( + + )} +
+
+
+ {localCode && localCode !== '{}' && ( +
+ { + if (value !== code) { + setHasBeenChanged(true); + } else { + setHasBeenChanged(false); + } + }} + onBlur={() => { + setLocalCode(codeRef.current.editor.getModel().getValue()); + }} + options={{ + scrollBeyondLastLine: isEditMode, + readOnly: !isEditMode, + theme: editorMode, + }} + /> +
+ )} + + ) : ( +
+
+
This schema has been enriched already with documentation.
+
+
+ )} + + ); +}; + +export default connector(EnrichedSchemaSection); diff --git a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaDescription.tsx b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaDescription.tsx index 673d8dda08..36ff6cf9ad 100644 --- a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaDescription.tsx +++ b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaDescription.tsx @@ -1,14 +1,14 @@ import React, {MutableRefObject, useEffect, useState} from 'react'; -import {getSchemaInfo} from '../../../../actions'; +import {getSchemaInfo, getSchemaVersions} from '../../../../actions'; import {connect, ConnectedProps} from 'react-redux'; import {ErrorPopUp} from 'components'; -import {calculateHeightOfCodeString} from '../../../../services'; import SchemaSection from './SchemaSection'; import styles from './index.module.scss'; -import {MessageSection, lastMessageMock} from './MessageSection'; +import EnrichedSchemaSection from './EnrichedSchemaSection'; const mapDispatchToProps = { getSchemaInfo, + getSchemaVersions, }; const connector = connect(null, mapDispatchToProps); @@ -19,15 +19,17 @@ type SchemaDescriptionProps = { setCode: (code: string) => void; wrapperSection: MutableRefObject; version: number; + versions: string[]; } & ConnectedProps; const SchemaDescription = (props: SchemaDescriptionProps) => { - const {schemaName, code, setCode, getSchemaInfo, wrapperSection, version} = props; + const {schemaName, code, setCode, getSchemaInfo, getSchemaVersions, wrapperSection, version, versions} = props; useEffect(() => { getSchemaInfo(schemaName).catch(() => { getSchemaInfo(schemaName + '-value'); }); + getSchemaVersions(schemaName); }, []); useEffect(() => { @@ -43,26 +45,14 @@ const SchemaDescription = (props: SchemaDescriptionProps) => { const [errorMessage, setErrorMessage] = useState(''); const [editorMode, setEditorMode] = useState(localStorage.getItem('theme') === 'dark' ? 'vs-dark' : 'vs'); - useEffect(() => { - if (firstTabSelected) { - recalculateContainerHeight(code); - } else { - recalculateContainerHeight(lastMessageMock); - } - }, [firstTabSelected, code]); - const setNewSchemaCode = (text: string) => { setCode(text); }; - const recalculateContainerHeight = (code: string) => { - const basicHeight = 50; - const headerHeight = 32; - if (wrapperSection && wrapperSection.current) { - wrapperSection.current.style.height = `${calculateHeightOfCodeString(code) + headerHeight + basicHeight}px`; - } else { - wrapperSection.current.style.height = `${basicHeight}px`; - } + const loadSchemaVersion = (version: string) => { + getSchemaInfo(schemaName, version).catch(() => { + getSchemaInfo(schemaName + '-value', version); + }); }; return ( @@ -76,17 +66,26 @@ const SchemaDescription = (props: SchemaDescriptionProps) => { setIsEditMode={setIsEditMode} setFirstTabSelected={setFirstTabSelected} editorMode={editorMode} - recalculateContainerHeight={recalculateContainerHeight} + wrapperSection={wrapperSection} setErrorMessage={setErrorMessage} setShowErrorPopUp={setShowErrorPopUp} version={version} + versions={versions} + loadSchemaVersion={loadSchemaVersion} /> ) : ( - )} {showErrorPopUp && setShowErrorPopUp(false)} />} diff --git a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaSection/index.tsx b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaSection/index.tsx index 701b6f136c..fad1bef22d 100644 --- a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaSection/index.tsx +++ b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaSection/index.tsx @@ -1,11 +1,11 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, {MutableRefObject, useEffect, useRef, useState} from 'react'; import MonacoEditor from '@uiw/react-monacoeditor'; import {calculateHeightOfCodeString, isJSON} from '../../../../../services'; import {useTranslation} from 'react-i18next'; -import {Button} from 'components'; -import styles from '../index.module.scss'; +import {Button, Dropdown} from 'components'; import {checkCompatibilityOfNewSchema, setSchemaSchema} from '../../../../../actions'; import {ConnectedProps, connect} from 'react-redux'; +import styles from '../index.module.scss'; const mapDispatchToProps = { setSchemaSchema, @@ -22,10 +22,12 @@ type SchemaSectionProps = { setIsEditMode: (flag: boolean) => void; setFirstTabSelected: (flag: boolean) => void; editorMode: string; - recalculateContainerHeight: (text: string) => void; + wrapperSection: MutableRefObject; setErrorMessage: (error: string) => void; setShowErrorPopUp: (flag: boolean) => void; version: number; + loadSchemaVersion: (version: string) => void; + versions: string[]; } & ConnectedProps; const SchemaSection = (props: SchemaSectionProps) => { @@ -37,12 +39,14 @@ const SchemaSection = (props: SchemaSectionProps) => { setIsEditMode, setFirstTabSelected, editorMode, - recalculateContainerHeight, + wrapperSection, checkCompatibilityOfNewSchema, setSchemaSchema, setErrorMessage, setShowErrorPopUp, version, + loadSchemaVersion, + versions, } = props; const [localCode, setLocalCode] = useState(code); @@ -60,6 +64,51 @@ const SchemaSection = (props: SchemaSectionProps) => { setIsEditMode(!isEditMode); }; + const recalculateContainerHeight = (code: string) => { + const basicHeight = 220; + if (wrapperSection && wrapperSection.current) { + wrapperSection.current.style.height = `${calculateHeightOfCodeString(code) + basicHeight}px`; + } else { + wrapperSection.current.style.height = `${basicHeight}px`; + } + if (!isEnrichmentAvailable(code)) { + if ((wrapperSection.current.style.height.replace('px', '') as number) > 600) { + wrapperSection.current.style.height = '600px'; + } + } else { + if ((wrapperSection.current.style.height.replace('px', '') as number) > 700) { + wrapperSection.current.style.height = '700px'; + } + } + }; + + const recalculateCodeHeight = (code: string) => { + const codeHeight = calculateHeightOfCodeString(code); + let height = 478; + if (!isEnrichmentAvailable(code)) { + height = 510; + } + if (codeHeight > height) { + return height; + } + return codeHeight; + }; + + const isEnrichmentAvailable = (code: string): boolean => { + let needsEnrichment = false; + const parsedCode = JSON.parse(code); + (parsedCode.fields || []).map(field => { + if (typeof field.type === 'object' && !Array.isArray(field.type)) { + if (!field.type.doc) { + needsEnrichment = true; + } + } else if (!field.doc) { + needsEnrichment = true; + } + }); + return needsEnrichment; + }; + const checkCompatibility = (_schemaName: string, _code: string, _version: number) => { checkCompatibilityOfNewSchema(_schemaName, _code, _version) .then(() => { @@ -99,14 +148,17 @@ const SchemaSection = (props: SchemaSectionProps) => { > Schema - {/* */} + />
+ {isEnrichmentAvailable(code) && ( + <> +
+
+
+ This schema can be automatically enriched with documentation and saved as a new version. +
+ +
+
Current schema:
+
+ + )} {code && code !== '{}' && ( { diff --git a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/index.module.scss b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/index.module.scss index 6edad57e01..27566454c2 100644 --- a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/index.module.scss +++ b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/index.module.scss @@ -59,3 +59,70 @@ flex: auto; padding-bottom: 8px; } + +.enrichmentContainer { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 16px; +} + +.enrichmentText { + padding: 8px; +} + +.enrichmentButton { + @include font-s; + border: none; + background-color: var(--color-code-no-edit); + color: var(--color-text-contrast); + font-weight: 600; + cursor: pointer; +} + +.enrichmentSchemaText { + padding: 8px; + @include font-s; + font-size: 12; + font-weight: 800; +} + +.codeContainer { + height: 100%; + width: 100%; + overflow: auto; + padding: 8px; +} + +.version { + display: flex; + p { + @include font-s; + font-size: 13px; + font-weight: 800; + padding: 8px 0; + margin-left: 16px; + color: var(--color-text-contrast); + } + button { + margin: 4px; + height: 24px; + width: 44px; + font-size: 12px; + align-items: center; + justify-content: center; + } + svg { + margin: 0 0 0 6px; + width: 11px; + height: 11px; + } + div { + font-size: 12px; + } +} + +.infoContainer { + display: flex; + background: transparent; +} diff --git a/frontend/control-center/src/pages/Schemas/SchemaItem/index.tsx b/frontend/control-center/src/pages/Schemas/SchemaItem/index.tsx index 071f3fcc8a..54367e9989 100644 --- a/frontend/control-center/src/pages/Schemas/SchemaItem/index.tsx +++ b/frontend/control-center/src/pages/Schemas/SchemaItem/index.tsx @@ -9,6 +9,7 @@ import SchemaDescription from './SchemaDescription/SchemaDescription'; const mapStateToProps = (state: StateModel) => { return { schemas: state.data.streams.schemas, + schemasVersions: state.data.streams.schemasVersions, }; }; @@ -32,6 +33,7 @@ const SchemaItem = (props: SchemaItemProps) => { addSchemasToSelection, itemSelected, setItemSelected, + schemasVersions, } = props; const [code, setCode] = useState(formatJSON(schemas[schemaName] ? schemas[schemaName].schema : '{}')); @@ -59,6 +61,10 @@ const SchemaItem = (props: SchemaItemProps) => { return 1; }; + const getVersions = (): string[] => { + return schemasVersions[schemaName] || []; + }; + return (
{ setCode={setCode} wrapperSection={wrapperSection} version={getVersion()} + versions={getVersions()} /> )}
diff --git a/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx b/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx index 26d4d42d4a..1a7d8f8dc9 100644 --- a/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx +++ b/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx @@ -40,7 +40,18 @@ const TopicDescription = (props: TopicDescriptionProps) => { } else { wrapperSection.current.style.height = `${basicHeight}px`; } + if ((wrapperSection.current.style.height.replace('px', '') as number) > 700) { + wrapperSection.current.style.height = '640px'; + } + } + }; + + const recalculateCodeHeight = (code: string) => { + const codeHeight = calculateHeightOfCodeString(code); + if (codeHeight > 478) { + return 478; } + return codeHeight; }; const calculateRetentionTime = (_jsonCode): string => { @@ -87,7 +98,7 @@ const TopicDescription = (props: TopicDescriptionProps) => { {expanded && ( { export const formatJSON = (jsonString: string): string => { if (jsonString) { - return JSON.stringify(JSON.parse(jsonString), null, 4); + return JSON.stringify(JSON.parse(jsonString), null, 2); } return ''; }; diff --git a/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss b/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss index acb3117dfe..924c4834d1 100644 --- a/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss +++ b/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss @@ -30,5 +30,5 @@ } .closeIcon { - width: 18px; + width: 12px; } diff --git a/lib/typescript/httpclient/src/client.ts b/lib/typescript/httpclient/src/client.ts index 9bc283d51c..dc67b2852b 100644 --- a/lib/typescript/httpclient/src/client.ts +++ b/lib/typescript/httpclient/src/client.ts @@ -37,6 +37,14 @@ import { GetStreamInfoPayload, DeleteStreamPayload, CreateStreamPayload, + LLMConsumersListPayload, + LLMInfoPayload, + LLMSStatsPayload, + LLMConsumersCreateRequestPayload, + LLMConsumersCreateResponsePayload, + LLMConsumersDeletePayload, + LLMQueryRequestPayload, + LLMQueryResponsePayload, } from './payload'; import { listChannelsDef, @@ -88,6 +96,12 @@ import { getStreamInfoDef, deleteStreamDef, createStreamDef, + llmConsumersListDef, + llmInfoDef, + llmStatsDef, + llmConsumersCreateDef, + llmConsumersDeleteDef, + llmQueryDef, } from './endpoints'; import 'isomorphic-fetch'; import FormData from 'form-data'; @@ -319,6 +333,20 @@ export class HttpClient { public createStream = this.getRequest(createStreamDef); + public getLLMInfo = this.getRequest(llmInfoDef); + + public getLLMStats = this.getRequest(llmStatsDef); + + public listLLMConsumers = this.getRequest(llmConsumersListDef); + + public deleteLLMConsumer = this.getRequest(llmConsumersDeleteDef); + + public createLLMConsumer = this.getRequest( + llmConsumersCreateDef + ); + + public llmQuery = this.getRequest(llmQueryDef); + private getRequest({endpoint, mapRequest, mapResponse}: EndpointDefinition): ApiRequest { return async (requestPayload: K) => { endpoint = typeof endpoint === 'string' ? endpoint : endpoint(requestPayload); diff --git a/lib/typescript/httpclient/src/endpoints/index.ts b/lib/typescript/httpclient/src/endpoints/index.ts index dae00d107f..8d847bac6d 100644 --- a/lib/typescript/httpclient/src/endpoints/index.ts +++ b/lib/typescript/httpclient/src/endpoints/index.ts @@ -47,3 +47,9 @@ export * from './getStreams'; export * from './getStreamInfo'; export * from './deleteStream'; export * from './createStream'; +export * from './llmConsumersCreate'; +export * from './llmConsumersList'; +export * from './llmStats'; +export * from './llmInfo'; +export * from './llmConsumersDelete'; +export * from './llmQuery'; diff --git a/lib/typescript/httpclient/src/endpoints/llmConsumersCreate.ts b/lib/typescript/httpclient/src/endpoints/llmConsumersCreate.ts new file mode 100644 index 0000000000..fe6fb40d57 --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmConsumersCreate.ts @@ -0,0 +1,11 @@ +import {LLMConsumersCreateRequestPayload} from '../payload'; + +export const llmConsumersCreateDef = { + endpoint: 'llm-consumers.create', + mapRequest: (request: LLMConsumersCreateRequestPayload) => ({ + name: request.name, + topic: request.topic, + textField: request.textField, + metadataFields: request.metadataFields, + }), +}; diff --git a/lib/typescript/httpclient/src/endpoints/llmConsumersDelete.ts b/lib/typescript/httpclient/src/endpoints/llmConsumersDelete.ts new file mode 100644 index 0000000000..c1a4add48d --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmConsumersDelete.ts @@ -0,0 +1,6 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const llmConsumersDeleteDef = { + endpoint: 'llm-consumers.delete', + mapResponse: response => camelcaseKeys(response), +}; diff --git a/lib/typescript/httpclient/src/endpoints/llmConsumersList.ts b/lib/typescript/httpclient/src/endpoints/llmConsumersList.ts new file mode 100644 index 0000000000..bcb4349afa --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmConsumersList.ts @@ -0,0 +1,6 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const llmConsumersListDef = { + endpoint: 'llm-consumers.list', + mapResponse: response => camelcaseKeys(response), +}; diff --git a/lib/typescript/httpclient/src/endpoints/llmInfo.ts b/lib/typescript/httpclient/src/endpoints/llmInfo.ts new file mode 100644 index 0000000000..4e21de899e --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmInfo.ts @@ -0,0 +1,6 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const llmInfoDef = { + endpoint: 'llm.info', + mapResponse: response => camelcaseKeys(response), +}; diff --git a/lib/typescript/httpclient/src/endpoints/llmQuery.ts b/lib/typescript/httpclient/src/endpoints/llmQuery.ts new file mode 100644 index 0000000000..37f2bba2d9 --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmQuery.ts @@ -0,0 +1,6 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const llmQueryDef = { + endpoint: 'llm.query', + mapResponse: response => camelcaseKeys(response), +}; diff --git a/lib/typescript/httpclient/src/endpoints/llmStats.ts b/lib/typescript/httpclient/src/endpoints/llmStats.ts new file mode 100644 index 0000000000..245a8526d1 --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmStats.ts @@ -0,0 +1,6 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const llmStatsDef = { + endpoint: 'llm.stats', + mapResponse: response => camelcaseKeys(response), +}; diff --git a/lib/typescript/httpclient/src/payload/LLMConsumerCreateRequestPayload.ts b/lib/typescript/httpclient/src/payload/LLMConsumerCreateRequestPayload.ts new file mode 100644 index 0000000000..acc1327d33 --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMConsumerCreateRequestPayload.ts @@ -0,0 +1,6 @@ +export interface LLMConsumersCreateRequestPayload { + name: string; + topic: string; + textField: string; + metadataFields: string[]; +} diff --git a/lib/typescript/httpclient/src/payload/LLMConsumerCreateResponsePayload.ts b/lib/typescript/httpclient/src/payload/LLMConsumerCreateResponsePayload.ts new file mode 100644 index 0000000000..5afcaf4bf4 --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMConsumerCreateResponsePayload.ts @@ -0,0 +1,3 @@ +export interface LLMConsumersCreateResponsePayload { + status: string; +} diff --git a/lib/typescript/httpclient/src/payload/LLMConsumerListPayload.ts b/lib/typescript/httpclient/src/payload/LLMConsumerListPayload.ts new file mode 100644 index 0000000000..f96c6697dc --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMConsumerListPayload.ts @@ -0,0 +1,10 @@ +interface LLMConsumer { + lag: number; + name: string; + status: string; + topic: string; +} + +export interface LLMConsumersListPayload { + data: LLMConsumer[]; +} diff --git a/lib/typescript/httpclient/src/payload/LLMConsumersDeletePayload.ts b/lib/typescript/httpclient/src/payload/LLMConsumersDeletePayload.ts new file mode 100644 index 0000000000..0c937869ae --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMConsumersDeletePayload.ts @@ -0,0 +1,3 @@ +export interface LLMConsumersDeletePayload { + name: string; +} diff --git a/lib/typescript/httpclient/src/payload/LLMInfoPayload.ts b/lib/typescript/httpclient/src/payload/LLMInfoPayload.ts new file mode 100644 index 0000000000..b053b5096d --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMInfoPayload.ts @@ -0,0 +1,8 @@ +interface LLMInfo { + llm: string; + llm_model: string; + vectorDatabase: string; +} +export interface LLMInfoPayload { + data: LLMInfo[]; +} diff --git a/lib/typescript/httpclient/src/payload/LLMQueryRequestPayload.ts b/lib/typescript/httpclient/src/payload/LLMQueryRequestPayload.ts new file mode 100644 index 0000000000..cb7b9ec7c4 --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMQueryRequestPayload.ts @@ -0,0 +1,3 @@ +export interface LLMQueryRequestPayload { + query: string; +} diff --git a/lib/typescript/httpclient/src/payload/LLMQueryResponsePayload.ts b/lib/typescript/httpclient/src/payload/LLMQueryResponsePayload.ts new file mode 100644 index 0000000000..6780fd96a0 --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMQueryResponsePayload.ts @@ -0,0 +1,6 @@ +export interface LLMQueryResponsePayload { + answer: { + query: string; + result: string; + }; +} diff --git a/lib/typescript/httpclient/src/payload/LLMStatsPayload.ts b/lib/typescript/httpclient/src/payload/LLMStatsPayload.ts new file mode 100644 index 0000000000..c0dda67a57 --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMStatsPayload.ts @@ -0,0 +1,3 @@ +export interface LLMSStatsPayload { + embeddings: number; +} diff --git a/lib/typescript/httpclient/src/payload/index.ts b/lib/typescript/httpclient/src/payload/index.ts index e1696f2dd1..94aa9a34aa 100644 --- a/lib/typescript/httpclient/src/payload/index.ts +++ b/lib/typescript/httpclient/src/payload/index.ts @@ -37,3 +37,11 @@ export * from './CreateTopicPayload'; export * from './GetStreamInfoPayload'; export * from './DeleteStreamPayload'; export * from './CreateStreamPayload'; +export * from './LLMConsumerCreateRequestPayload'; +export * from './LLMConsumerCreateResponsePayload'; +export * from './LLMConsumerListPayload'; +export * from './LLMInfoPayload'; +export * from './LLMStatsPayload'; +export * from './LLMConsumersDeletePayload'; +export * from './LLMQueryResponsePayload'; +export * from './LLMQueryRequestPayload'; diff --git a/lib/typescript/model/Streams.ts b/lib/typescript/model/Streams.ts index aee9ed5127..749f887f95 100644 --- a/lib/typescript/model/Streams.ts +++ b/lib/typescript/model/Streams.ts @@ -8,6 +8,9 @@ export interface Streams { schemas: { [topicName: string]: Schema; }; + schemasVersions: { + [topicName: string]: string[]; + }; streamsInfo: { [streamName: string]: StreamInfo; }; diff --git a/lib/typescript/translations/translations.ts b/lib/typescript/translations/translations.ts index 89529fc011..938f8780d1 100644 --- a/lib/typescript/translations/translations.ts +++ b/lib/typescript/translations/translations.ts @@ -608,6 +608,16 @@ const resources = { noWebhooksText: `You don't have any Webhooks installed, please `, customHeader: 'Customer Header', signKey: 'Sign key', + noLLMs: 'No LLMs Found', + noLLMsText: `You don't have any LLMs installed, please `, + noLLMConsumers: 'No LLM Consumers Found', + noLLMConsumersText: `You don't have any LLM Consumers installed, please `, + llmConsumerNameExplanation: 'The name of the LLM Consumer', + llmConsumerTopicNameExplanation: 'The name of the topic to which the LLM Consumer is subscribed', + llmConsumerTextFieldExplanation: 'The text field of the LLM Consumer', + llmConsumerMetadataFieldsExplanation: 'The metadata fields of the LLM Consumer', + llmConsumerCreatedSuccessfully: 'LLM Consumer created successfully', + llmConsumerTypeExplanation: 'The type serialization of the LLM Consumer', }, }, de: { @@ -1229,6 +1239,16 @@ const resources = { noWebhooksText: 'Sie haben keine Webhooks installiert, bitte ', customHeader: 'Kundenkopfzeile', signKey: 'Signierschlüssel', + noLLMs: 'Keine LLMs gefunden', + noLLMsText: `Sie haben keine LLMs installiert, bitte `, + noLLMConsumers: 'Keine LLM Consumers gefunden', + noLLMConsumersText: `Sie haben keine LLM Consumers installiert, bitte `, + llmConsumerNameExplanation: 'Der Name des LLM Consumer', + llmConsumerTopicNameExplanation: 'Der Name des LLM Consumer Topics', + llmConsumerTextFieldExplanation: 'Das Textfeld des LLM Consumer', + llmConsumerMetadataFieldsExplanation: 'Die Metadatenfelder des LLM Consumer', + llmConsumerCreatedSuccessfully: 'LLM Consumer erfolgreich erstellt', + llmConsumerTypeExplanation: 'Der Typ des LLM Consumer', }, }, fr: { @@ -1843,6 +1863,16 @@ const resources = { noWebhooksText: `Vous n'avez pas de Webhooks installé, veuillez vous `, customHeader: 'En-tête du client', signKey: 'Touche de signature', + noLLMs: 'Pas de LLMs trouvés', + noLLMsText: `Vous n'avez pas de LLMs installé, veuillez vous `, + noLLMConsumers: 'Pas de LLM Consumers trouvés', + noLLMConsumersText: `Vous n'avez pas de LLM Consumers installé, veuillez vous `, + llmConsumerNameExplanation: 'Le nom du LLM Consumer', + llmConsumerTopicNameExplanation: 'Le nom du topic du LLM Consumer', + llmConsumerTextFieldExplanation: 'Le champ de texte du LLM Consumer', + llmConsumerMetadataFieldsExplanation: 'Les champs de métadonnées du LLM Consumer', + llmConsumerCreatedSuccessfully: 'LLM Consumer créé avec succès', + llmConsumerTypeExplanation: 'Le type du LLM Consumer', }, }, es: { @@ -2460,6 +2490,16 @@ const resources = { noWebhooksText: 'No tiene instalado ningún Webhooks, por favor, ', customHeader: 'Cabecera del cliente', signKey: 'Clave de la firma', + noLLMs: 'No se han encontrado LLMs', + noLLMsText: `No tiene instalado ningún LLMs, por favor, `, + noLLMConsumers: 'No se han encontrado LLM Consumers', + noLLMConsumersText: `No tiene instalado ningún LLM Consumer, por favor, `, + llmConsumerNameExplanation: 'El nombre del LLM Consumer', + llmConsumerTopicNameExplanation: 'El nombre del topic del LLM Consumer', + llmConsumerTextFieldExplanation: 'El campo de texto del LLM Consumer', + llmConsumerMetadataFieldsExplanation: 'Los campos de metadatos del LLM Consumer', + llmConsumerCreatedSuccessfully: 'LLM Consumer creado con éxito', + llmConsumerTypeExplanation: 'El tipo de serialización del LLM Consumer', }, }, };