diff --git a/app/cdap/api/app.js b/app/cdap/api/app.js index 3cf32e81b12..4f9064c6ca2 100644 --- a/app/cdap/api/app.js +++ b/app/cdap/api/app.js @@ -29,4 +29,6 @@ export const MyAppApi = { batchStatus: apiCreator(dataSrc, 'POST', 'POLL', '/namespaces/:namespace/status'), batchAppDetail: apiCreator(dataSrc, 'POST', 'REQUEST', '/namespaces/:namespace/appdetail'), delete: apiCreator(dataSrc, 'DELETE', 'REQUEST', appPath), + summarize: apiCreator(dataSrc, 'POST', 'REQUEST', `${appPath}/summarize`), + summarizeDraft: apiCreator(dataSrc, 'POST', 'REQUEST', `${basepath}/summarize`), }; diff --git a/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineDetailsButtons/PipelineGenaiSummaryButton.tsx b/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineDetailsButtons/PipelineGenaiSummaryButton.tsx new file mode 100644 index 00000000000..46b41f523a4 --- /dev/null +++ b/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineDetailsButtons/PipelineGenaiSummaryButton.tsx @@ -0,0 +1,92 @@ +/* + * Copyright © 2024 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import React, { useEffect, useRef, useState } from 'react'; +import classnames from 'classnames'; +import GraphicEqIcon from '@material-ui/icons/GraphicEq'; +import T from 'i18n-react'; +import { PrimaryTextLowercaseButton } from 'components/shared/Buttons/PrimaryTextLowercaseButton'; +import styled from 'styled-components'; +import { PipelineGenaiSummaryModal } from '../PipelineGenaiSummaryModal'; +import { MyAppApi } from 'api/app'; +import { getCurrentNamespace } from 'services/NamespaceStore'; + +const PREFIX = 'features.PipelineDetails.TopPanel'; + +interface IPipelineGenaiSummaryButtonProps { + pipelineName: string; +} + +const StyledSummarizeIcon = styled(GraphicEqIcon)` + margin-bottom: -4px; +`; + +const GenaiButton = styled.div` + padding: 0; +`; + +export const PipelineGenaiSummaryButton = ({ pipelineName }: IPipelineGenaiSummaryButtonProps) => { + const [isOpen, setIsOpen] = useState(false); + const [summary, setSummary] = useState(''); + const buttonRef = useRef(null); + + useEffect(() => { + MyAppApi.summarize({ + namespace: getCurrentNamespace(), + appId: pipelineName, + }).subscribe((res) => { + setSummary(res); + }); + }, []); + + const toggleButton = () => { + setIsOpen(!isOpen); + }; + + const renderPipelineGenaiSummaryBtn = () => { + return ( + + +
+ +
{T.translate(`${PREFIX}.genaiSummarize`)}
+
+
+
+ ); + }; + + return ( +
+ {renderPipelineGenaiSummaryBtn()} + setIsOpen(false)} + anchorEl={buttonRef.current} + pipelineName={pipelineName} + summary={summary} + /> +
+ ); +}; diff --git a/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineDetailsButtons/index.js b/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineDetailsButtons/index.js index 3fc1c040b56..e89896f99b2 100644 --- a/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineDetailsButtons/index.js +++ b/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineDetailsButtons/index.js @@ -29,7 +29,11 @@ import ApolloClient from 'apollo-boost'; import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import introspectionQueryResultData from '../../../../../../graphql/fragments/fragmentTypes.json'; import SessionTokenStore from 'services/SessionTokenStore'; -import { useFeatureFlagDefaultFalse } from 'services/react/customHooks/useFeatureFlag'; +import { + useFeatureFlagDefaultFalse, + useFeatureFlagDefaultTrue, +} from 'services/react/customHooks/useFeatureFlag'; +import { PipelineGenaiSummaryButton } from './PipelineGenaiSummaryButton'; const fragmentMatcher = new IntrospectionFragmentMatcher({ introspectionQueryResultData, @@ -106,6 +110,8 @@ export default function PipelineDetailsButtons({ const lifecycleManagementEditEnabled = useFeatureFlagDefaultFalse( 'lifecycle.management.edit.enabled' ); + // TODO: replace with useFeatureFlagDefaultFalse when productionising + const genaiPipelineSummaryEnabled = useFeatureFlagDefaultTrue('genai.pipeline.summary.enabled'); return ( @@ -142,6 +148,9 @@ export default function PipelineDetailsButtons({ /> {lifecycleManagementEditEnabled && } + {genaiPipelineSummaryEnabled && ( + + )} diff --git a/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineGenaiSummaryModal.tsx b/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineGenaiSummaryModal.tsx new file mode 100644 index 00000000000..4e3d3d00c9a --- /dev/null +++ b/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineGenaiSummaryModal.tsx @@ -0,0 +1,85 @@ +/* + * Copyright © 2024 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import T from 'i18n-react'; +import PipelineModeless from 'components/PipelineDetails/PipelineModeless'; +import MarkdownWithStyles from 'components/shared/Markdown'; +import LoadingSVG from 'components/shared/LoadingSVG'; + +const PREFIX = 'features.PipelineDetails.TopPanel'; + +interface IPipelineGenaiSummaryModalProps { + pipelineName?: string; + summary: string; + anchorEl: any; + onClose: () => void; + isOpen: boolean; +} + +const SummaryContainer = styled.div` + height: 100%; + width: 100%; + max-width: 880px; + max-height: min(60vh, 800px); + padding: 0 20px; + overflow: auto; +`; + +const PopperClass = styled.div` + max-width: 880px; +`; + +const LoadingContainer = styled.div` + width: 100%; + height: 100px; + display: flex; + align-items: center; + justify-content: center; +`; + +export const PipelineGenaiSummaryModal = ({ + pipelineName, + summary, + anchorEl, + onClose, + isOpen, +}: IPipelineGenaiSummaryModalProps) => { + return ( + + + {summary ? ( + + ) : ( + + + + )} + + + ); +}; diff --git a/app/cdap/components/hydrator/components/TopPanel/ActionButtons/index.tsx b/app/cdap/components/hydrator/components/TopPanel/ActionButtons/index.tsx index 5b51761eafa..06120ca9504 100644 --- a/app/cdap/components/hydrator/components/TopPanel/ActionButtons/index.tsx +++ b/app/cdap/components/hydrator/components/TopPanel/ActionButtons/index.tsx @@ -15,7 +15,8 @@ */ import IconSVG from 'components/shared/IconSVG'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import GraphicEqIcon from '@material-ui/icons/GraphicEq'; import { objectQuery } from 'services/helpers'; import PipelineConfigurationsStore, { ACTIONS as PipelineConfigurationsActions, @@ -24,6 +25,7 @@ import { PipelineConfigure } from '../PipelineConfigure'; import { ActionButtonsContainer, BorderRightButton, + BorderRightOnlyButton, ButtonLabel, CommonButton, CustomTooltip, @@ -36,6 +38,8 @@ import { PreviewModeButton, RunTimeSpan, } from './styles'; +import { useFeatureFlagDefaultTrue } from 'services/react/customHooks/useFeatureFlag'; +import { PipelineGenaiSummaryModal } from 'components/PipelineDetails/PipelineDetailsTopPanel/PipelineGenaiSummaryModal'; interface IDuration { minutes: string; @@ -53,6 +57,7 @@ interface IActionButtonsProps { toggleScheduler: () => void; hasNodes: boolean; onSaveDraft: () => void; + onPipelineSummarize: (summarySetter: (string) => void) => void; onPublish: () => void; onImport: () => void; onFileSelect: (files: FileList) => void; @@ -90,6 +95,7 @@ export const ActionButtons = ({ toggleScheduler, hasNodes, onSaveDraft, + onPipelineSummarize, onPublish, onImport, onFileSelect, @@ -116,6 +122,20 @@ export const ActionButtons = ({ getStoreConfig, }: IActionButtonsProps) => { const [selectedFile, setSelectedFile] = useState(null); + const [genaiSummary, setGenaiSummary] = useState(''); + const [isSummaryModalOpen, setSummaryModalOpen] = useState(false); + const pipelineGenaiSummaryButtonRef = useRef(null); + // TODO: replace with useFeatureFlagDefaultFalse when productionising + const genaiPipelineSummaryEnabled = useFeatureFlagDefaultTrue('genai.pipeline.summary.enabled'); + + const onGenaiSummary = () => { + if (!genaiPipelineSummaryEnabled) { + return; + } + setGenaiSummary(''); + onPipelineSummarize(setGenaiSummary); + setSummaryModalOpen(true); + }; const handleFile = (event) => { if (!objectQuery(event, 'target', 'files', 0)) { @@ -365,6 +385,27 @@ export const ActionButtons = ({ + {genaiPipelineSummaryEnabled && ( + + + +
+ + Summarize +
+
+
+
+ )} )} @@ -384,6 +425,14 @@ export const ActionButtons = ({ validatePluginProperties={validatePluginProperties} getRuntimeArgs={getRuntimeArgs} > + {hasNodes && pipelineGenaiSummaryButtonRef.current && ( + setSummaryModalOpen(false)} + summary={genaiSummary} + /> + )} ); }; diff --git a/app/cdap/components/hydrator/components/TopPanel/ActionButtons/styles.ts b/app/cdap/components/hydrator/components/TopPanel/ActionButtons/styles.ts index 474e174bbe3..27e2d2b651d 100644 --- a/app/cdap/components/hydrator/components/TopPanel/ActionButtons/styles.ts +++ b/app/cdap/components/hydrator/components/TopPanel/ActionButtons/styles.ts @@ -51,6 +51,11 @@ export const BorderRightButton = styled(BaseButton)` border-right: 1px solid ${greyBorder}; `; +export const BorderRightOnlyButton = styled(BorderRightButton)` + border-left: none; + position: relative; +`; + export const PreviewModeButton = styled(BaseButton)` background-color: ${previewActive}; `; diff --git a/app/cdap/components/hydrator/components/TopPanel/NameAndDescription/styles.ts b/app/cdap/components/hydrator/components/TopPanel/NameAndDescription/styles.ts index 33bfcb58056..0e390942879 100644 --- a/app/cdap/components/hydrator/components/TopPanel/NameAndDescription/styles.ts +++ b/app/cdap/components/hydrator/components/TopPanel/NameAndDescription/styles.ts @@ -129,7 +129,7 @@ export const DescriptionTextField = styled(TextField)` `; export const EditStatus = styled.div` - width: calc(20vw - 110px); + width: calc(20vw - 180px); flex-wrap: wrap; display: flex; background-color: transparent; diff --git a/app/cdap/components/hydrator/components/TopPanel/TopPanel.tsx b/app/cdap/components/hydrator/components/TopPanel/TopPanel.tsx index d153ba57dbb..44d4a3707e3 100644 --- a/app/cdap/components/hydrator/components/TopPanel/TopPanel.tsx +++ b/app/cdap/components/hydrator/components/TopPanel/TopPanel.tsx @@ -31,6 +31,7 @@ import { editPipeline } from 'services/PipelineUtils'; import downloadFile from 'services/download-file'; import { cleanseAndCompareTwoObjects } from 'services/helpers'; import ThemeWrapper from 'components/ThemeWrapper'; +import { FeatureProvider } from 'services/react/providers/featureFlagProvider'; export interface IGlobalObj { etlRealtime?: string; @@ -129,6 +130,7 @@ interface ITopPanelProps { toggleScheduler: () => void; hasNodes: boolean; onSaveDraft: () => void; + onPipelineSummarize: (summarySetter: (string) => void) => void; onPublish: (isEdit) => void; onImport: () => void; onFileSelect: (files: FileList) => void; @@ -181,6 +183,7 @@ export const TopPanel = ({ closeScheduler, hasNodes, onSaveDraft, + onPipelineSummarize, onPublish, onImport, onFileSelect, @@ -380,108 +383,111 @@ export const TopPanel = ({ /> ); return ( - - {errorState.errorMessage && ( - { - errorDispatch({ type: 'reset' }); - }} + + + {errorState.errorMessage && ( + { + errorDispatch({ type: 'reset' }); + }} + /> + )} + + + + + + {viewScheduler && ( + + )} + {viewLogs && ( + + + + )} + {(viewLogs || viewConfig) && ( + { + onCloseLog(); + if (viewConfig) { + toggleConfig(); + } + }} + > + )} + - )} - - - - - - {viewScheduler && ( - - )} - {viewLogs && ( - - - - )} - {(viewLogs || viewConfig) && ( - { - onCloseLog(); - if (viewConfig) { - toggleConfig(); - } - }} - > - )} - - + + ); }; diff --git a/app/cdap/text/text-en.yaml b/app/cdap/text/text-en.yaml index 1bdf52a0f8f..75e24c847be 100644 --- a/app/cdap/text/text-en.yaml +++ b/app/cdap/text/text-en.yaml @@ -2527,6 +2527,9 @@ features: edit: Edit export: Export exportModalTitle: Export pipeline configuration + genaiSummarize: Summarize + genaiSummaryOfPipeline: Summary of {pipelineName} + genaiSummaryOfUnnamedPipeline: Pipeline summary history: History run: Run schedule: Schedule diff --git a/app/hydrator/controllers/create/toppanel-ctrl.js b/app/hydrator/controllers/create/toppanel-ctrl.js index 3999cca705a..6f66ff5b49a 100644 --- a/app/hydrator/controllers/create/toppanel-ctrl.js +++ b/app/hydrator/controllers/create/toppanel-ctrl.js @@ -31,6 +31,7 @@ class HydratorPlusPlusTopPanelCtrl { HydratorPlusPlusPreviewActions, $interval, myPipelineApi, + myAppsApi, $state, myAlertOnValium, MY_CONFIG, @@ -63,6 +64,7 @@ class HydratorPlusPlusTopPanelCtrl { this.previewActions = HydratorPlusPlusPreviewActions; this.$interval = $interval; this.myPipelineApi = myPipelineApi; + this.myAppsApi = myAppsApi; this.myPreviewLogsApi = myPreviewLogsApi; this.myPreferenceApi = myPreferenceApi; this.DAGPlusPlusNodesStore = DAGPlusPlusNodesStore; @@ -98,6 +100,7 @@ class HydratorPlusPlusTopPanelCtrl { this.toggleSchedulerV2 = this.toggleSchedulerV2.bind(this); this.closeScheduler = this.closeScheduler.bind(this); this.onSaveDraftV2 = this.onSaveDraftV2.bind(this); + this.onPipelineSummarize = this.onPipelineSummarize.bind(this); this.onPublishV2 = this.onPublishV2.bind(this); this.onImportV2 = this.onImportV2.bind(this); this.onExportV2 = this.onExportV2.bind(this); @@ -571,7 +574,7 @@ class HydratorPlusPlusTopPanelCtrl { this.HydratorPlusPlusConfigStore.getDraftId() ); this.$window.localStorage.setItem("LastPreviewId", this.currentPreviewId); - } + } onClickLogs() { this.viewLogs = !this.viewLogs; } @@ -1156,6 +1159,15 @@ class HydratorPlusPlusTopPanelCtrl { ); } + onPipelineSummarize(summarySetter) { + let pipelineConfig = this.HydratorPlusPlusConfigStore.getConfigForExport(); + this.myAppsApi.summarize({ + namespace: this.$state.params.namespace, + }, pipelineConfig).$promise.then((res) => { + summarySetter(res); + }); + } + runPreview() { this.previewLoading = true; this.loadingLabel = "Starting"; diff --git a/app/hydrator/templates/create/reacttoppanel.html b/app/hydrator/templates/create/reacttoppanel.html index 700d27d8b9a..d11ce24e7b5 100644 --- a/app/hydrator/templates/create/reacttoppanel.html +++ b/app/hydrator/templates/create/reacttoppanel.html @@ -35,6 +35,7 @@ close-scheduler="HydratorPlusPlusTopPanelCtrl.closeScheduler" has-nodes="HydratorPlusPlusTopPanelCtrl.hasNodes" on-save-draft="HydratorPlusPlusTopPanelCtrl.onSaveDraftV2" + on-pipeline-summarize="HydratorPlusPlusTopPanelCtrl.onPipelineSummarize" on-publish="HydratorPlusPlusTopPanelCtrl.onPublishV2" on-import="HydratorPlusPlusTopPanelCtrl.onImportV2" on-file-select="HydratorPlusPlusTopPanelCtrl.importFile2" diff --git a/app/services/apps/my-apps-api.js b/app/services/apps/my-apps-api.js index 016edaad302..d32bdb624c9 100644 --- a/app/services/apps/my-apps-api.js +++ b/app/services/apps/my-apps-api.js @@ -30,6 +30,7 @@ angular.module(PKG.name + '.services') { delete: myHelpers.getConfig('DELETE', 'REQUEST', detailPath), list: myHelpers.getConfig('GET', 'REQUEST', listPath, true), - get: myHelpers.getConfig('GET', 'REQUEST', detailPath) + get: myHelpers.getConfig('GET', 'REQUEST', detailPath), + summarize: myHelpers.getConfig('POST', 'REQUEST', basePath + '/summarize'), }); });