From 08acbd2afd8347818b5e4e4384c2aeed060fc923 Mon Sep 17 00:00:00 2001 From: Armen Derikyan <82438895+Derikyan@users.noreply.github.com> Date: Mon, 27 Jan 2025 19:56:30 +0400 Subject: [PATCH 1/2] fix(chat): fix max headers size (Issue #2983) (#3015) --- apps/chat/.env.development | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/chat/.env.development b/apps/chat/.env.development index f5df3acc6..832df58a6 100644 --- a/apps/chat/.env.development +++ b/apps/chat/.env.development @@ -101,6 +101,9 @@ RECENT_MODELS_IDS="gpt-35-turbo,gpt-4,epam10k-semantic-search,gpt-world,mirror" RECENT_ADDONS_IDS="addon-epam10k-golden-qna,addon-epam10k-semantic-search" PUBLICATION_FILTERS="title,job title,role,dial_roles" +# Default header size limit aligned with Docker's default value for consistency +NODE_OPTIONS="--max-http-header-size=32768" + # Themes # THEMES_CONFIG_HOST="" From b2f78e0d9315ea1889f72362baa5d05c86d8a3c0 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Mon, 27 Jan 2025 18:28:47 +0100 Subject: [PATCH 2/2] feat(chat): Add the application shared with the user to a "My workspace" (Issue #2955, #2965, #2971, #3004) (#3008) --- apps/chat/src/components/Chat/ModelList.tsx | 2 +- .../Chat/Publish/PublicationChatControls.tsx | 6 +- .../Chat/Publish/PublicationHandler.tsx | 11 +- .../components/Chat/TalkTo/TalkToModal.tsx | 2 +- .../CodeAppView/CodeAppView.tsx | 4 + .../CodeAppView/CodeEditor.tsx | 3 + .../CodeAppView/SourceFilesEditor.tsx | 41 +++-- .../components/Files/FileItemContextMenu.tsx | 11 +- .../Marketplace/ApplicationCard.tsx | 2 +- .../ApplicationDetails/ApplicationFooter.tsx | 57 +++---- .../ApplicationDetails/ApplicationHeader.tsx | 78 --------- .../components/Marketplace/TabRenderer.tsx | 2 +- .../store/application/application.epics.ts | 47 ++++-- .../store/application/application.reducers.ts | 5 +- .../src/store/codeEditor/codeEditor.epics.ts | 16 +- apps/chat/src/store/files/files.epics.ts | 2 + apps/chat/src/store/files/files.reducers.ts | 9 ++ apps/chat/src/store/models/models.epics.ts | 2 +- apps/chat/src/store/models/models.reducers.ts | 45 +++--- apps/chat/src/store/models/models.types.ts | 6 + apps/chat/src/store/share/share.epics.ts | 153 ++++++++++++------ apps/chat/src/store/share/share.reducers.ts | 98 ++--------- apps/chat/src/store/share/share.selectors.ts | 72 +++++++++ apps/chat/src/store/share/share.types.ts | 28 ++++ apps/chat/src/utils/app/data/file-service.ts | 3 +- apps/chat/src/utils/app/data/share-service.ts | 90 +++++++---- .../src/utils/app/data/text-file-service.ts | 20 ++- apps/chat/src/utils/app/folders.ts | 25 +++ 28 files changed, 494 insertions(+), 346 deletions(-) create mode 100644 apps/chat/src/store/share/share.selectors.ts create mode 100644 apps/chat/src/store/share/share.types.ts diff --git a/apps/chat/src/components/Chat/ModelList.tsx b/apps/chat/src/components/Chat/ModelList.tsx index e4a6544d2..f6adaf1b4 100644 --- a/apps/chat/src/components/Chat/ModelList.tsx +++ b/apps/chat/src/components/Chat/ModelList.tsx @@ -354,7 +354,7 @@ export const ModelList = ({ const handleEdit = useCallback( (currentEntity: DialAIEntityModel) => { - dispatch(ApplicationActions.get(currentEntity.id)); + dispatch(ApplicationActions.get({ applicationId: currentEntity.id })); handleOpenApplicationModal(getApplicationType(currentEntity)); }, [dispatch, handleOpenApplicationModal], diff --git a/apps/chat/src/components/Chat/Publish/PublicationChatControls.tsx b/apps/chat/src/components/Chat/Publish/PublicationChatControls.tsx index 2449b7a9d..23a74064c 100644 --- a/apps/chat/src/components/Chat/Publish/PublicationChatControls.tsx +++ b/apps/chat/src/components/Chat/Publish/PublicationChatControls.tsx @@ -133,9 +133,9 @@ export function PublicationControlsView< unselectConversation(); unselectPrompt(); dispatch( - ApplicationActions.get( - resourcesToReview[publicationIdx + offset].reviewUrl, - ), + ApplicationActions.get({ + applicationId: resourcesToReview[publicationIdx + offset].reviewUrl, + }), ); dispatch(PublicationActions.setIsApplicationReview(true)); } diff --git a/apps/chat/src/components/Chat/Publish/PublicationHandler.tsx b/apps/chat/src/components/Chat/Publish/PublicationHandler.tsx index 6f407877e..c21bd6bd2 100644 --- a/apps/chat/src/components/Chat/Publish/PublicationHandler.tsx +++ b/apps/chat/src/components/Chat/Publish/PublicationHandler.tsx @@ -284,13 +284,10 @@ export function PublicationHandler({ publication }: Props) { }; const startApplicationsReview = () => { - dispatch( - ApplicationActions.get( - applicationsToReviewIds.length - ? applicationsToReviewIds[0].reviewUrl - : reviewedApplicationsIds[0].reviewUrl, - ), - ); + const applicationId = applicationsToReviewIds.length + ? applicationsToReviewIds[0].reviewUrl + : reviewedApplicationsIds[0].reviewUrl; + dispatch(ApplicationActions.get({ applicationId })); dispatch(PublicationActions.setIsApplicationReview(true)); }; diff --git a/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx b/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx index d7cdc32eb..48391334b 100644 --- a/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx +++ b/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx @@ -229,7 +229,7 @@ const TalkToModalView = ({ const handleEditApplication = useCallback( (entity: DialAIEntityModel) => { - dispatch(ApplicationActions.get(entity.id)); + dispatch(ApplicationActions.get({ applicationId: entity.id })); setEditModel(entity); }, [dispatch], diff --git a/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeAppView.tsx b/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeAppView.tsx index 4fbc2f67b..f59046111 100644 --- a/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeAppView.tsx +++ b/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeAppView.tsx @@ -342,6 +342,10 @@ export const CodeAppView: FC = ({ label={t('Select folder with source files')} rules={validators['sources']} error={errors.sources?.message || errors.sourceFiles?.message} + disabled={isSharedWithMe} + tooltip={ + isSharedWithMe ? getSharedTooltip('folder with source files') : '' + } /> {sources && } diff --git a/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeEditor.tsx b/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeEditor.tsx index da90187be..b068ecbeb 100644 --- a/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeEditor.tsx +++ b/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeEditor.tsx @@ -20,6 +20,7 @@ import { constructPath } from '@/src/utils/app/file'; import { getChildAndCurrentFoldersIdsById, getNextDefaultName, + splitEntityId, } from '@/src/utils/app/folders'; import { getIdWithoutRootPathSegments } from '@/src/utils/app/id'; @@ -394,6 +395,7 @@ export const CodeEditor = ({ sourcesFolderId }: Props) => { const handleUploadEmptyFile = useCallback( (fileName: string) => { if (fileName && sourcesFolderId) { + const { bucket } = splitEntityId(sourcesFolderId); dispatch( FilesActions.uploadFile({ fileContent: new File([''], fileName, { @@ -402,6 +404,7 @@ export const CodeEditor = ({ sourcesFolderId }: Props) => { relativePath: getIdWithoutRootPathSegments(sourcesFolderId), id: constructPath(sourcesFolderId, fileName), name: fileName, + bucket, }), ); setNewFileFolder(undefined); diff --git a/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/SourceFilesEditor.tsx b/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/SourceFilesEditor.tsx index d36965027..355986b68 100644 --- a/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/SourceFilesEditor.tsx +++ b/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/SourceFilesEditor.tsx @@ -24,12 +24,16 @@ interface SourceFilesEditorProps { value?: string; onChange?: (v: string) => void; error?: string; + tooltip?: string; + disabled?: boolean; } const _SourceFilesEditor: FC = ({ value, onChange, error, + tooltip, + disabled, }) => { const { t } = useTranslation(Translation.Marketplace); @@ -77,26 +81,31 @@ const _SourceFilesEditor: FC = ({ > {value ? getIdWithoutRootPathSegments(value) : t('No folder')} -
- - {value ? t('Change') : t('Add')} - - {value && ( + +
- )} -
+ {value && ( + + )} +
+ diff --git a/apps/chat/src/components/Files/FileItemContextMenu.tsx b/apps/chat/src/components/Files/FileItemContextMenu.tsx index a1b0c6016..e56fc1e69 100644 --- a/apps/chat/src/components/Files/FileItemContextMenu.tsx +++ b/apps/chat/src/components/Files/FileItemContextMenu.tsx @@ -9,6 +9,7 @@ import { MouseEvent, MouseEventHandler, useMemo } from 'react'; import { useTranslation } from 'next-i18next'; +import { isCurrentFolderOrParentSharedWithMeAndCanEdit } from '@/src/utils/app/folders'; import { isMyEntity } from '@/src/utils/app/id'; import { FeatureType } from '@/src/types/common'; @@ -17,6 +18,7 @@ import { DisplayMenuItemProps } from '@/src/types/menu'; import { Translation } from '@/src/types/translation'; import { CodeEditorSelectors } from '@/src/store/codeEditor/codeEditor.reducer'; +import { FilesSelectors } from '@/src/store/files/files.reducers'; import { useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; @@ -61,6 +63,8 @@ export function FileItemContextMenu({ ); const isCodeEditorFile = !!useAppSelector(selectFileContentSelector); + const folders = useAppSelector(FilesSelectors.selectFolders); + const menuItems: DisplayMenuItemProps[] = useMemo( () => [ { @@ -111,7 +115,11 @@ export function FileItemContextMenu({ { name: t('Delete'), dataQa: 'delete', - display: isMyEntity(file, FeatureType.File) || !!file.sharedWithMe, + display: + isMyEntity(file, FeatureType.File) || + !!file.sharedWithMe || + isCurrentFolderOrParentSharedWithMeAndCanEdit(folders, file.folderId), + Icon: IconTrashX, onClick: onDelete, }, @@ -125,6 +133,7 @@ export function FileItemContextMenu({ onUnshare, isPublishingConversationEnabled, onUnpublish, + folders, onDelete, onOpenChange, ], diff --git a/apps/chat/src/components/Marketplace/ApplicationCard.tsx b/apps/chat/src/components/Marketplace/ApplicationCard.tsx index 9ce75bf1a..56236355d 100644 --- a/apps/chat/src/components/Marketplace/ApplicationCard.tsx +++ b/apps/chat/src/components/Marketplace/ApplicationCard.tsx @@ -339,7 +339,7 @@ export const ApplicationCard = ({ triggerIconSize={18} className="m-0 xl:invisible group-hover:xl:visible" /> - {!isMyApp && ( + {!isMyApp && !entity.sharedWithMe && ( )} - {isMyApp ? ( - - - - ) : ( - - + + ) : ( + - - - - )} + + + ))} {isApplicationId(entity.id) && (isMyApp || isPublicApp) && ( diff --git a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx index abeea518a..1421a2878 100644 --- a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx +++ b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx @@ -59,34 +59,6 @@ export const ApplicationDetailsHeader = ({ entity }: Props) => { const isApplicationsSharingEnabled = useAppSelector((state) => SettingsSelectors.isFeatureEnabled(state, Feature.ApplicationsSharing), ); - // const dispatch = useAppDispatch(); - - // const contextMenuItems = useMemo( - // () => [ - // { - // BrandIcon: IconLink, - // text: t('Copy link'), - // onClick: () => { - // dispatch(UIActions.showInfoToast(t('Link copied'))); - // }, - // }, - // { - // BrandIcon: IconBrandFacebook, - // text: t('Share via Facebook'), - // onClick: () => { - // return 'Share via Facebook'; - // }, - // }, - // { - // BrandIcon: IconBrandX, - // text: t('Share via X'), - // onClick: () => { - // return 'Share via X'; - // }, - // }, - // ], - // [dispatch, t], - // ); return (
@@ -130,57 +102,7 @@ export const ApplicationDetailsHeader = ({ entity }: Props) => { - {/*
- - - {t('Share')} - - } - > -
-
- -
{entity.name}
-
-
- {contextMenuItems.map(({ BrandIcon, text, ...props }) => ( - - - {text} - - } - className="flex w-full items-center gap-3 px-3 py-2 hover:bg-accent-primary-alpha" - {...props} - /> - ))} -
-
-
- -
*/} - {/*

- {application.title} -

*/} {isMyApp && isApplicationsSharingEnabled && ( diff --git a/apps/chat/src/components/Marketplace/TabRenderer.tsx b/apps/chat/src/components/Marketplace/TabRenderer.tsx index c4296d5e4..6ef72cee1 100644 --- a/apps/chat/src/components/Marketplace/TabRenderer.tsx +++ b/apps/chat/src/components/Marketplace/TabRenderer.tsx @@ -313,7 +313,7 @@ export const TabRenderer = ({ screenState }: TabRendererProps) => { const handleEditApplication = useCallback( (entity: DialAIEntityModel) => { - dispatch(ApplicationActions.get(entity.id)); + dispatch(ApplicationActions.get({ applicationId: entity.id })); setApplicationModel({ entity, action: ApplicationActionType.EDIT, diff --git a/apps/chat/src/store/application/application.epics.ts b/apps/chat/src/store/application/application.epics.ts index 7737b4520..556103679 100644 --- a/apps/chat/src/store/application/application.epics.ts +++ b/apps/chat/src/store/application/application.epics.ts @@ -1,5 +1,6 @@ import { EMPTY, + Observable, concat, concatMap, from, @@ -10,6 +11,8 @@ import { } from 'rxjs'; import { catchError, filter, map, switchMap } from 'rxjs/operators'; +import { AnyAction } from '@reduxjs/toolkit'; + import { combineEpics } from 'redux-observable'; import { regenerateApplicationId } from '@/src/utils/app/application'; @@ -33,6 +36,7 @@ import { DeleteType } from '@/src/constants/marketplace'; import { ApplicationActions } from '../application/application.reducers'; import { AuthSelectors } from '../auth/auth.reducers'; import { ModelsActions, ModelsSelectors } from '../models/models.reducers'; +import { ShareActions, ShareSelectors } from '../share/share.reducers'; const createApplicationEpic: AppEpic = (action$) => action$.pipe( @@ -179,17 +183,42 @@ const getApplicationEpic: AppEpic = (action$, state$) => action$.pipe( filter(ApplicationActions.get.match), switchMap(({ payload }) => - ApplicationService.get(payload).pipe( - map((application) => { + ApplicationService.get(payload.applicationId).pipe( + switchMap((application) => { + if (!application) { + return of(ApplicationActions.getFail()); + } + const modelsMap = ModelsSelectors.selectModelsMap(state$.value); - return application - ? ApplicationActions.getSuccess({ + const modelFromState = modelsMap[application.reference]; + + const actions: Observable[] = []; + actions.push( + of( + ApplicationActions.getSuccess({ ...application, - sharedWithMe: modelsMap[application.reference]?.sharedWithMe, - permissions: modelsMap[application.reference]?.permissions, - isShared: modelsMap[application.reference]?.isShared, - }) - : ApplicationActions.getFail(); + sharedWithMe: modelFromState?.sharedWithMe, + permissions: modelFromState?.permissions, + isShared: modelFromState?.isShared, + }), + ), + ); + + if (payload.isForSharing) { + const permissionsFromState = ShareSelectors.selectSharePermissions( + state$.value, + ); + actions.push( + of( + ShareActions.shareApplication({ + resourceId: application.id, + permissions: permissionsFromState, + }), + ), + ); + } + + return concat(...actions); }), catchError((err) => { console.error('Failed to get application:', err); diff --git a/apps/chat/src/store/application/application.reducers.ts b/apps/chat/src/store/application/application.reducers.ts index 066ddab0a..05e983097 100644 --- a/apps/chat/src/store/application/application.reducers.ts +++ b/apps/chat/src/store/application/application.reducers.ts @@ -73,7 +73,10 @@ export const applicationSlice = createSlice({ updateFail: (state) => { state.appLoading = UploadStatus.FAILED; }, - get: (state, _action: PayloadAction) => { + get: ( + state, + _action: PayloadAction<{ applicationId: string; isForSharing?: boolean }>, + ) => { state.appLoading = UploadStatus.LOADING; }, getSuccess: (state, action: PayloadAction) => { diff --git a/apps/chat/src/store/codeEditor/codeEditor.epics.ts b/apps/chat/src/store/codeEditor/codeEditor.epics.ts index be76e5d6c..410c1ac1b 100644 --- a/apps/chat/src/store/codeEditor/codeEditor.epics.ts +++ b/apps/chat/src/store/codeEditor/codeEditor.epics.ts @@ -17,6 +17,7 @@ import { combineEpics } from 'redux-observable'; import { FileService } from '@/src/utils/app/data/file-service'; import { TextFileService } from '@/src/utils/app/data/text-file-service'; +import { splitEntityId } from '@/src/utils/app/folders'; import { getIdWithoutRootPathSegments } from '@/src/utils/app/id'; import { translate } from '@/src/utils/app/translation'; @@ -189,12 +190,15 @@ const updateFileContentEpic: AppEpic = (action$, state$) => return EMPTY; } - return TextFileService.updateContent( - file.relativePath ?? getIdWithoutRootPathSegments(file.id), - file.name, - payload.content, - file.contentType, - ).pipe( + const { bucket } = splitEntityId(file.id); + return TextFileService.updateContent({ + relativePath: + file.relativePath ?? getIdWithoutRootPathSegments(file.id), + fileName: file.name, + content: payload.content, + contentType: file.contentType, + bucket, + }).pipe( filter(({ success }) => !!success), switchMap(({ success }) => { if (success) { diff --git a/apps/chat/src/store/files/files.epics.ts b/apps/chat/src/store/files/files.epics.ts index a8ff1cfdc..cb3762f82 100644 --- a/apps/chat/src/store/files/files.epics.ts +++ b/apps/chat/src/store/files/files.epics.ts @@ -59,6 +59,8 @@ const uploadFileEpic: AppEpic = (action$) => formData, payload.relativePath, payload.name, + undefined, + payload.bucket, ).pipe( filter( ({ percent, result }) => diff --git a/apps/chat/src/store/files/files.reducers.ts b/apps/chat/src/store/files/files.reducers.ts index 55fc56446..c543d787c 100644 --- a/apps/chat/src/store/files/files.reducers.ts +++ b/apps/chat/src/store/files/files.reducers.ts @@ -64,6 +64,7 @@ export const filesSlice = createSlice({ id: string; relativePath?: string; name: string; + bucket?: string; }>, ) => { state.files = state.files.filter((file) => file.id !== payload.id); @@ -547,6 +548,13 @@ const selectInitialized = createSelector( (state) => state.initialized, ); +const selectFolderById = createSelector( + [selectFolders, (_state, folderId: string) => folderId], + (folders, folderId) => { + return folders.find((folder) => folder.id == folderId); + }, +); + export const FilesSelectors = { selectFiles, selectFilteredFiles, @@ -559,6 +567,7 @@ export const FilesSelectors = { selectAreFoldersLoading, selectLoadingFolderIds, selectNewAddedFolderId, + selectFolderById, selectFilesByIds, selectFileById, selectFoldersWithSearchTerm, diff --git a/apps/chat/src/store/models/models.epics.ts b/apps/chat/src/store/models/models.epics.ts index 53feb0d4a..7376082c9 100644 --- a/apps/chat/src/store/models/models.epics.ts +++ b/apps/chat/src/store/models/models.epics.ts @@ -176,7 +176,7 @@ const getInstalledModelIdsEpic: AppEpic = (action$, state$) => const allModels = ModelsSelectors.selectModels(state$.value); return allModels - .filter((model) => isMyApplication(model)) + .filter((model) => isMyApplication(model) || model.sharedWithMe) .map((app) => app.reference); }), switchMap((myAppIds) => { diff --git a/apps/chat/src/store/models/models.reducers.ts b/apps/chat/src/store/models/models.reducers.ts index d13855c62..6726c9c63 100644 --- a/apps/chat/src/store/models/models.reducers.ts +++ b/apps/chat/src/store/models/models.reducers.ts @@ -3,7 +3,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { combineEntities } from '@/src/utils/app/common'; import { translate } from '@/src/utils/app/translation'; -import { ApplicationInfo, ApplicationStatus } from '@/src/types/applications'; +import { ApplicationStatus } from '@/src/types/applications'; import { ErrorMessage } from '@/src/types/error'; import { DialAIEntityModel, @@ -16,7 +16,7 @@ import { errorsMessages } from '@/src/constants/errors'; import { DeleteType } from '@/src/constants/marketplace'; import * as ModelsSelectors from './models.selectors'; -import { ModelsState } from './models.types'; +import { ModelUpdatedValues, ModelsState } from './models.types'; import { UploadStatus } from '@epam/ai-dial-shared'; import cloneDeep from 'lodash-es/cloneDeep'; @@ -274,31 +274,32 @@ export const modelsSlice = createSlice({ { payload, }: PayloadAction<{ - reference: string; - updatedValues: Partial; + modelsToUpdate: ModelUpdatedValues[]; }>, ) => { - const model = state.modelsMap[payload.reference]; + payload.modelsToUpdate.forEach((modelToUpdate) => { + const model = state.modelsMap[modelToUpdate.reference]; - if (model) { - const updatedModel = { - ...model, - ...payload.updatedValues, - }; - state.modelsMap[model.reference] = updatedModel; - state.modelsMap[model.id] = updatedModel; + if (model) { + const updatedModel = { + ...model, + ...modelToUpdate.updatedValues, + }; + state.modelsMap[model.reference] = updatedModel; + state.modelsMap[model.id] = updatedModel; - state.models = state.models.map((model) => { - if (model.reference === payload.reference) { - return { - ...model, - ...payload.updatedValues, - }; - } + state.models = state.models.map((modelFromState) => { + if (modelFromState.reference === modelToUpdate.reference) { + return { + ...modelFromState, + ...modelToUpdate.updatedValues, + }; + } - return model; - }); - } + return modelFromState; + }); + } + }); }, }, }); diff --git a/apps/chat/src/store/models/models.types.ts b/apps/chat/src/store/models/models.types.ts index 4ee7761ed..f82ce8e67 100644 --- a/apps/chat/src/store/models/models.types.ts +++ b/apps/chat/src/store/models/models.types.ts @@ -1,3 +1,4 @@ +import { ApplicationInfo } from '@/src/types/applications'; import { ErrorMessage } from '@/src/types/error'; import { DialAIEntityModel, @@ -21,3 +22,8 @@ export interface ModelsState { publishRequestModels: PublishRequestDialAIEntityModel[]; publishedApplicationIds: string[]; } + +export interface ModelUpdatedValues { + reference: string; + updatedValues: Partial; +} diff --git a/apps/chat/src/store/share/share.epics.ts b/apps/chat/src/store/share/share.epics.ts index aa72f2c9a..95562df1e 100644 --- a/apps/chat/src/store/share/share.epics.ts +++ b/apps/chat/src/store/share/share.epics.ts @@ -16,6 +16,7 @@ import { AnyAction } from '@reduxjs/toolkit'; import { combineEpics } from 'redux-observable'; +import { getApplicationType } from '@/src/utils/app/application'; import { ConversationService } from '@/src/utils/app/data/conversation-service'; import { ShareService } from '@/src/utils/app/data/share-service'; import { @@ -35,6 +36,7 @@ import { hasWritePermission } from '@/src/utils/app/share'; import { translate } from '@/src/utils/app/translation'; import { ApiUtils, parseConversationApiKey } from '@/src/utils/server/api'; +import { ApplicationType } from '@/src/types/applications'; import { Conversation } from '@/src/types/chat'; import { FeatureType } from '@/src/types/common'; import { DialFile } from '@/src/types/files'; @@ -52,7 +54,11 @@ import { DEFAULT_CONVERSATION_NAME } from '@/src/constants/default-ui-settings'; import { errorsMessages } from '@/src/constants/errors'; import { DeleteType } from '@/src/constants/marketplace'; -import { ApplicationSelectors } from '../application/application.reducers'; +import { + ApplicationActions, + ApplicationSelectors, +} from '../application/application.reducers'; +import { CodeEditorActions } from '../codeEditor/codeEditor.reducer'; import { ConversationsActions, ConversationsSelectors, @@ -60,6 +66,7 @@ import { import { FilesActions, FilesSelectors } from '../files/files.reducers'; import { MarketplaceActions } from '../marketplace/marketplace.reducers'; import { ModelsActions, ModelsSelectors } from '../models/models.reducers'; +import { ModelUpdatedValues } from '../models/models.types'; import { PromptsActions, PromptsSelectors } from '../prompts/prompts.reducers'; import { SettingsSelectors } from '../settings/settings.reducers'; import { UIActions } from '../ui/ui.reducers'; @@ -293,6 +300,30 @@ const shareApplicationEpic: AppEpic = (action$, state$) => action$.pipe( filter(ShareActions.shareApplication.match), switchMap(({ payload }) => { + const modelsMap = ModelsSelectors.selectModelsMap(state$.value); + const application = modelsMap[payload.resourceId]; + + if (!application) { + return of(ShareActions.shareFail()); + } + + const applicationType = getApplicationType(application); + const applicationDetails = ApplicationSelectors.selectApplicationDetail( + state$.value, + ); + + if ( + applicationType === ApplicationType.CODE_APP && + applicationDetails?.reference !== application.reference + ) { + return of( + ApplicationActions.get({ + applicationId: payload.resourceId, + isForSharing: true, + }), + ); + } + const resources: ShareResource[] = [ { url: ApiUtils.encodeApiUrl(payload.resourceId), @@ -300,18 +331,15 @@ const shareApplicationEpic: AppEpic = (action$, state$) => }, ]; - const applicationDetails = ApplicationSelectors.selectApplicationDetail( - state$.value, - ); - - if (applicationDetails?.iconUrl) { + if (application?.iconUrl) { resources.push({ - url: ApiUtils.encodeApiUrl(applicationDetails.iconUrl), + url: ApiUtils.encodeApiUrl(application.iconUrl), }); } if ( hasWritePermission(payload.permissions) && + applicationType && applicationDetails?.function?.sourceFolder ) { resources.push({ @@ -504,7 +532,9 @@ const triggerGettingSharedListingsAttachmentsEpic: AppEpic = ( (action) => (FilesActions.getFilesWithFolders.match(action) && !action.payload.id) || - ShareActions.acceptShareInvitationSuccess.match(action), + ShareActions.acceptShareInvitationSuccess.match(action) || + ShareActions.triggerGettingSharedFilesListings.match(action) || + CodeEditorActions.initCodeEditor.match(action), ), filter(() => { return SettingsSelectors.isSharingEnabled(state$.value, FeatureType.Chat); @@ -879,50 +909,71 @@ const getSharedListingSuccessEpic: AppEpic = (action$, state$) => if (payload.featureType === FeatureType.Application) { const modelsMap = ModelsSelectors.selectModelsMap(state$.value); if (payload.sharedWith === ShareRelations.others) { - actions.push( - ...(payload.resources.entities - .map((sharedItem) => { - const sharedModel = modelsMap[sharedItem.id]; - - if (sharedModel) { - return ModelsActions.updateLocalModels({ - reference: sharedModel.reference, - updatedValues: { - isShared: true, - }, - }); - } - return undefined; - }) - .filter(Boolean) as AnyAction[]), - ); + const modelsToUpdate = payload.resources.entities + .map((sharedItem) => { + const sharedModel = modelsMap[sharedItem.id]; + + if (sharedModel) { + return { + reference: sharedModel.reference, + updatedValues: { + isShared: true, + }, + }; + } + return undefined; + }) + .filter(Boolean) as ModelUpdatedValues[]; + + actions.push(ModelsActions.updateLocalModels({ modelsToUpdate })); } else { //TODO make request for the shared applications to add them into the state when share invitation is accepted. //TODO new action-service needs to be created. - //TODO add all shared with me agents to installedModels - // const sharedReferences: string[] = []; //part of TODO uncomment or remove if not needed; + const updateSharedActions: AnyAction[] = []; + const modelsToUpdate = payload.resources.entities + .map((sharedItem) => { + const sharedModel = modelsMap[sharedItem.id]; - actions.push( - ...(payload.resources.entities - .map((sharedItem) => { - const sharedModel = modelsMap[sharedItem.id]; + if (sharedModel) { + return { + reference: sharedModel.reference, + updatedValues: { + sharedWithMe: true, + permissions: sharedItem.permissions, + }, + }; + } + return undefined; + }) + .filter(Boolean) as ModelUpdatedValues[]; + + if (modelsToUpdate.length) { + updateSharedActions.push( + ModelsActions.updateLocalModels({ modelsToUpdate }), + ); - if (sharedModel) { - // sharedReferences.push(sharedModel.reference); //part of TODO uncomment or remove if not needed; + updateSharedActions.push(ModelsActions.getInstalledModelIds()); - return ModelsActions.updateLocalModels({ - reference: sharedModel.reference, - updatedValues: { - sharedWithMe: true, - permissions: sharedItem.permissions, - }, - }); - } - return undefined; - }) - .filter(Boolean) as AnyAction[]), - ); + const { acceptedId } = ShareSelectors.selectAcceptedEntityInfo( + state$.value, + ); + + const acceptedApplicationReference = + acceptedId && modelsMap[acceptedId]?.reference; + + if (acceptedApplicationReference) { + updateSharedActions.push( + MarketplaceActions.setDetailsModel({ + reference: acceptedApplicationReference, + isSuggested: false, + }), + ); + updateSharedActions.push(ShareActions.resetAcceptedEntityInfo()); + } + + actions.push(...updateSharedActions); + } } } @@ -1010,10 +1061,14 @@ const revokeAccessSuccessEpic: AppEpic = (action$, state$) => } return of( ModelsActions.updateLocalModels({ - reference: applicationReference, - updatedValues: { - isShared: false, - }, + modelsToUpdate: [ + { + reference: applicationReference, + updatedValues: { + isShared: false, + }, + }, + ], }), ); } diff --git a/apps/chat/src/store/share/share.reducers.ts b/apps/chat/src/store/share/share.reducers.ts index 00bcc9ddf..e4ff45fbe 100644 --- a/apps/chat/src/store/share/share.reducers.ts +++ b/apps/chat/src/store/share/share.reducers.ts @@ -1,4 +1,4 @@ -import { PayloadAction, createSelector, createSlice } from '@reduxjs/toolkit'; +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { splitEntityId } from '@/src/utils/app/folders'; import { hasWritePermission } from '@/src/utils/app/share'; @@ -9,14 +9,14 @@ import { import { ApplicationInfo } from '@/src/types/applications'; import { FeatureType } from '@/src/types/common'; -import { ErrorMessage } from '@/src/types/error'; import { DialFile } from '@/src/types/files'; import { FolderInterface } from '@/src/types/folder'; import { ModalState } from '@/src/types/modal'; import { Prompt } from '@/src/types/prompt'; import { ShareRelations } from '@/src/types/share'; -import { RootState } from '../index'; +import * as ShareSelectors from './share.selectors'; +import { ShareState } from './share.types'; import { ConversationInfo, @@ -25,23 +25,8 @@ import { UploadStatus, } from '@epam/ai-dial-shared'; -export interface ShareState { - initialized: boolean; - status: UploadStatus; - error: ErrorMessage | undefined; - invitationId: string | undefined; - writeInvitationId: string | undefined; - shareResourceName: string | undefined; - shareResourceId: string | undefined; - shareModalState: ModalState; - unshareEntity?: Omit; - acceptedId: string | undefined; - isFolderAccepted: boolean | undefined; - shareFeatureType?: FeatureType; - shareIsFolder?: boolean; - isConversation?: boolean; - isPrompt?: boolean; -} +export { ShareSelectors }; +export type { ShareState }; const initialState: ShareState = { initialized: false, @@ -86,6 +71,7 @@ export const shareSlice = createSlice({ state.shareFeatureType = payload.featureType; state.shareIsFolder = payload.isFolder; state.shareResourceId = payload.resourceId; + state.sharePermissions = payload.permissions; const name = splitEntityId(payload.resourceId).name; state.shareResourceName = @@ -121,12 +107,16 @@ export const shareSlice = createSlice({ ) => state, shareApplication: ( state, - _action: PayloadAction<{ + { + payload, + }: PayloadAction<{ resourceId: string; permissions?: SharePermission[]; }>, ) => { state.shareModalState = ModalState.LOADING; + state.sharePermissions = payload.permissions; + state.shareResourceId = payload.resourceId; }, shareSuccess: ( state, @@ -144,10 +134,12 @@ export const shareSlice = createSlice({ } state.shareModalState = ModalState.OPENED; + state.sharePermissions = undefined; }, shareFail: (state, _action: PayloadAction) => { state.invitationId = undefined; state.shareModalState = ModalState.CLOSED; + state.sharePermissions = undefined; }, revokeAccess: ( @@ -226,6 +218,7 @@ export const shareSlice = createSlice({ }, triggerGettingSharedConversationListings: (state) => state, triggerGettingSharedPromptListings: (state) => state, + triggerGettingSharedFilesListings: (state) => state, triggerGettingSharedApplicationsListings: (state) => state, acceptShareInvitationFail: ( state, @@ -266,67 +259,4 @@ export const shareSlice = createSlice({ }, }); -const rootSelector = (state: RootState): ShareState => state.share; - -const selectInvitationId = createSelector([rootSelector], (state) => { - return state.invitationId; -}); - -const selectWriteInvitationId = createSelector([rootSelector], (state) => { - return state.writeInvitationId; -}); - -const selectShareModalState = createSelector([rootSelector], (state) => { - return state.shareModalState; -}); - -const selectShareModalClosed = createSelector([rootSelector], (state) => { - return state.shareModalState === ModalState.CLOSED; -}); - -const selectUnshareModel = createSelector([rootSelector], (state) => { - return state.unshareEntity; -}); - -const selectShareResourceId = createSelector([rootSelector], (state) => { - return state.shareResourceId; -}); - -const selectShareResourceName = createSelector([rootSelector], (state) => { - return state.shareResourceName; -}); - -const selectShareFeatureType = createSelector([rootSelector], (state) => { - return state.shareFeatureType; -}); -const selectShareIsFolder = createSelector([rootSelector], (state) => { - return state.shareIsFolder; -}); -const selectAcceptedEntityInfo = createSelector([rootSelector], (state) => { - return { - acceptedId: state.acceptedId, - isFolderAccepted: state.isFolderAccepted, - isConversation: state.isConversation, - isPrompt: state.isPrompt, - }; -}); -const selectInitialized = createSelector( - [rootSelector], - (state) => state.initialized, -); - -export const ShareSelectors = { - selectInvitationId, - selectWriteInvitationId, - selectShareModalState, - selectShareModalClosed, - selectUnshareModel, - selectShareResourceName, - selectShareResourceId, - selectAcceptedEntityInfo, - selectShareFeatureType, - selectShareIsFolder, - selectInitialized, -}; - export const ShareActions = shareSlice.actions; diff --git a/apps/chat/src/store/share/share.selectors.ts b/apps/chat/src/store/share/share.selectors.ts new file mode 100644 index 000000000..7a5b96110 --- /dev/null +++ b/apps/chat/src/store/share/share.selectors.ts @@ -0,0 +1,72 @@ +import { createSelector } from '@reduxjs/toolkit'; + +import { ModalState } from '@/src/types/modal'; + +import { RootState } from '..'; +import { ShareState } from './share.types'; + +const rootSelector = (state: RootState): ShareState => state.share; + +export const selectInvitationId = createSelector([rootSelector], (state) => { + return state.invitationId; +}); + +export const selectWriteInvitationId = createSelector( + [rootSelector], + (state) => { + return state.writeInvitationId; + }, +); + +export const selectShareModalState = createSelector([rootSelector], (state) => { + return state.shareModalState; +}); +export const selectShareModalClosed = createSelector( + [rootSelector], + (state) => { + return state.shareModalState === ModalState.CLOSED; + }, +); + +export const selectShareResourceId = createSelector([rootSelector], (state) => { + return state.shareResourceId; +}); + +export const selectShareResourceName = createSelector( + [rootSelector], + (state) => { + return state.shareResourceName; + }, +); + +export const selectShareFeatureType = createSelector( + [rootSelector], + (state) => { + return state.shareFeatureType; + }, +); +export const selectShareIsFolder = createSelector([rootSelector], (state) => { + return state.shareIsFolder; +}); +export const selectAcceptedEntityInfo = createSelector( + [rootSelector], + (state) => { + return { + acceptedId: state.acceptedId, + isFolderAccepted: state.isFolderAccepted, + isConversation: state.isConversation, + isPrompt: state.isPrompt, + }; + }, +); +export const selectInitialized = createSelector( + [rootSelector], + (state) => state.initialized, +); +export const selectSharePermissions = createSelector( + [rootSelector], + (state) => state.sharePermissions, +); +export const selectUnshareModel = createSelector([rootSelector], (state) => { + return state.unshareEntity; +}); diff --git a/apps/chat/src/store/share/share.types.ts b/apps/chat/src/store/share/share.types.ts new file mode 100644 index 000000000..eaa2e650b --- /dev/null +++ b/apps/chat/src/store/share/share.types.ts @@ -0,0 +1,28 @@ +import { FeatureType } from '@/src/types/common'; +import { ErrorMessage } from '@/src/types/error'; +import { ModalState } from '@/src/types/modal'; + +import { + ShareEntity, + SharePermission, + UploadStatus, +} from '@epam/ai-dial-shared'; + +export interface ShareState { + initialized: boolean; + status: UploadStatus; + error: ErrorMessage | undefined; + invitationId: string | undefined; + writeInvitationId: string | undefined; + shareResourceName: string | undefined; + shareResourceId: string | undefined; + shareModalState: ModalState; + unshareEntity?: Omit; + acceptedId: string | undefined; + isFolderAccepted: boolean | undefined; + shareFeatureType?: FeatureType; + shareIsFolder?: boolean; + isConversation?: boolean; + isPrompt?: boolean; + sharePermissions?: SharePermission[]; +} diff --git a/apps/chat/src/utils/app/data/file-service.ts b/apps/chat/src/utils/app/data/file-service.ts index 856e0e740..4a1d49315 100644 --- a/apps/chat/src/utils/app/data/file-service.ts +++ b/apps/chat/src/utils/app/data/file-service.ts @@ -39,9 +39,10 @@ export class FileService { relativePath: string | undefined, fileName: string, httpMethod?: HTTPMethod, + bucket?: string, ): Observable<{ percent?: number; result?: DialFile }> { const resultPath = ApiUtils.encodeApiUrl( - constructPath(getFileRootId(), relativePath, fileName), + constructPath(getFileRootId(bucket), relativePath, fileName), ); return ApiUtils.requestOld({ diff --git a/apps/chat/src/utils/app/data/share-service.ts b/apps/chat/src/utils/app/data/share-service.ts index 5596b9c7d..d294a9a78 100644 --- a/apps/chat/src/utils/app/data/share-service.ts +++ b/apps/chat/src/utils/app/data/share-service.ts @@ -35,6 +35,22 @@ import { EnumMapper } from '../mappers'; import { ConversationInfo } from '@epam/ai-dial-shared'; import { contentType } from 'mime-types'; +export const getFolderFromShareResult = ( + folder: BackendChatFolder, + apiKeyType: ApiKeys, +) => { + const id = ApiUtils.decodeApiUrl(folder.url.slice(0, folder.url.length - 1)); + const { apiKey, bucket, parentPath } = splitEntityId(id); + + return { + id, + name: folder.name, + folderId: constructPath(apiKey, bucket, parentPath), + type: EnumMapper.getFolderTypeByApiKey(apiKeyType), + permissions: folder.permissions, + }; +}; + export class ShareService { public static share( shareData: ShareRequestModel, @@ -126,17 +142,11 @@ export class ShareService { } if (entity.nodeType === BackendDataNodeType.FOLDER) { const folder = conversationResource as BackendChatFolder; - const id = ApiUtils.decodeApiUrl( - folder.url.slice(0, folder.url.length - 1), + const conversationFolder = getFolderFromShareResult( + folder, + ApiKeys.Conversations, ); - const { apiKey, bucket, parentPath } = splitEntityId(id); - - folders.push({ - id, - name: folder.name, - folderId: constructPath(apiKey, bucket, parentPath), - type: EnumMapper.getFolderTypeByApiKey(ApiKeys.Conversations), - }); + folders.push(conversationFolder); } } @@ -159,23 +169,20 @@ export class ShareService { } if (entity.nodeType === BackendDataNodeType.FOLDER) { const folder = promptResource as BackendChatFolder; - const id = ApiUtils.decodeApiUrl( - folder.url.slice(0, folder.url.length - 1), + const promptFolder = getFolderFromShareResult( + folder, + ApiKeys.Prompts, ); - const { apiKey, bucket, parentPath } = splitEntityId(id); - - folders.push({ - id, - name: folder.name, - folderId: constructPath(apiKey, bucket, parentPath), - type: EnumMapper.getFolderTypeByApiKey(ApiKeys.Prompts), - }); + folders.push(promptFolder); } } if (entity.resourceType === BackendResourceType.FILE) { + const fileResource = entity as + | BackendChatEntity + | BackendChatFolder; if (entity.nodeType === BackendDataNodeType.ITEM) { - const file = entity as BackendEntity; + const file = fileResource as BackendEntity; const id = ApiUtils.decodeApiUrl(file.url); const { apiKey, bucket, parentPath } = splitEntityId(id); @@ -189,18 +196,41 @@ export class ShareService { contentType: mimeType ? mimeType : 'application/octet-stream', }); } + + if (entity.nodeType === BackendDataNodeType.FOLDER) { + const folder = fileResource as BackendChatFolder; + const fileFolder = getFolderFromShareResult( + folder, + ApiKeys.Files, + ); + folders.push(fileFolder); + } } if (entity.resourceType === BackendResourceType.APPLICATION) { - const application = entity as BackendEntity; - const id = ApiUtils.decodeApiUrl(application.url); - - entities.push({ - name: application.name, - version: parseApplicationApiKey(application.name).version, - id, - permissions: application.permissions, - }); + const applicationResource = entity as + | BackendChatEntity + | BackendChatFolder; + if (entity.nodeType === BackendDataNodeType.ITEM) { + const application = applicationResource as BackendEntity; + const id = ApiUtils.decodeApiUrl(application.url); + + entities.push({ + name: application.name, + version: parseApplicationApiKey(application.name).version, + id, + permissions: application.permissions, + }); + } + + if (entity.nodeType === BackendDataNodeType.FOLDER) { + const folder = applicationResource as BackendChatFolder; + const fileFolder = getFolderFromShareResult( + folder, + ApiKeys.Files, + ); + folders.push(fileFolder); + } } }); diff --git a/apps/chat/src/utils/app/data/text-file-service.ts b/apps/chat/src/utils/app/data/text-file-service.ts index ea816a686..1d6902fcf 100644 --- a/apps/chat/src/utils/app/data/text-file-service.ts +++ b/apps/chat/src/utils/app/data/text-file-service.ts @@ -21,12 +21,19 @@ export class TextFileService { ); } - public static updateContent( - relativePath: string, - fileName: string, - content: string, - contentType: string, - ): Observable { + public static updateContent({ + relativePath, + fileName, + content, + contentType, + bucket, + }: { + relativePath: string; + fileName: string; + content: string; + contentType: string; + bucket?: string; + }): Observable { const formData = new FormData(); formData.append( 'attachments', @@ -39,6 +46,7 @@ export class TextFileService { relativePath, fileName, HTTPMethod.PUT, + bucket, ).pipe( filter( ({ result, percent }) => diff --git a/apps/chat/src/utils/app/folders.ts b/apps/chat/src/utils/app/folders.ts index 7284a5522..67a43fd3c 100644 --- a/apps/chat/src/utils/app/folders.ts +++ b/apps/chat/src/utils/app/folders.ts @@ -20,6 +20,7 @@ import { updateEntitiesFoldersAndIds, } from './common'; import { isRootId } from './id'; +import { hasWritePermission } from './share'; import { Attachment, @@ -599,3 +600,27 @@ export const renameFolderWithChildren = ({ return updatedFolders.concat(newFolder); }; + +export const isCurrentFolderOrParentSharedWithMeAndCanEdit = ( + folders: FolderInterface[], + folderId: string | undefined, +): boolean => { + if (!folderId) { + return false; + } + + let folder = folders.find((folder) => folder.id === folderId); + + if (folder?.sharedWithMe && hasWritePermission(folder.permissions)) { + return true; + } + + while (folder) { + folder = folders.find((item) => item.id === folder!.folderId); + + if (folder?.sharedWithMe && hasWritePermission(folder.permissions)) { + return true; + } + } + return false; +};