From c5190f839ece85d42010c760e95e71d86c1d7df3 Mon Sep 17 00:00:00 2001 From: baaalint Date: Wed, 11 Sep 2024 11:48:16 +0200 Subject: [PATCH 1/4] feat(admin): add documents table --- ui/src/App.tsx | 2 + ui/src/atoms/documents.ts | 47 ++++ .../feature/AddEditDocumentModal.tsx | 102 ++++++++ ui/src/components/feature/DocumentsTable.tsx | 222 ++++++++++++++++++ ui/src/components/feature/Layout.tsx | 1 + ui/src/pages/DocumentsTablePage.tsx | 24 ++ ui/src/services/Api.ts | 48 +++- ui/src/shared/types/document.ts | 26 ++ 8 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 ui/src/atoms/documents.ts create mode 100644 ui/src/components/feature/AddEditDocumentModal.tsx create mode 100644 ui/src/components/feature/DocumentsTable.tsx create mode 100644 ui/src/pages/DocumentsTablePage.tsx create mode 100644 ui/src/shared/types/document.ts diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 894f16c..f3964f3 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -19,6 +19,7 @@ import { ChatHistoriesTablePage } from 'pages/ChatHistoriesPage' import { ChatPage } from 'pages/ChatPage' import { DataSourceTablePage } from 'pages/DataSourcesTablePage' import { DatasetsTablePage } from 'pages/DatasetsTablePage' +import { DocumentsTablePage } from 'pages/DocumentsTablePage' import { LoginPage } from 'pages/LoginPage' import { ModelsTablePage } from 'pages/ModelsTablePage' import { ProjectsTablePage } from 'pages/ProjectsTablePage' @@ -41,6 +42,7 @@ function App() { { path: '/admin/data-sources', element: }, { path: '/admin/datasets', element: }, { path: '/admin/models', element: }, + { path: '/admin/documents', element: }, { path: '/admin/histories', element: diff --git a/ui/src/atoms/documents.ts b/ui/src/atoms/documents.ts new file mode 100644 index 0000000..e47f465 --- /dev/null +++ b/ui/src/atoms/documents.ts @@ -0,0 +1,47 @@ +// Copyright 2024 Iguazio +// +// 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 Client from '@services/Api'; +import { Document } from '@shared/types/document'; +import { atom } from 'jotai'; + +export const documentsAtom = atom([]); + +export const documentsLoadingAtom = atom(false); + +export const documentsErrorAtom = atom(null); + + +export const documentsWithFetchAtom = atom( + (get) => get(documentsAtom), + async (_get, set, username) => { + set(documentsLoadingAtom, true); + set(documentsErrorAtom, null); + try { + const documents = await Client.getDocuments(username as string); + const sortedDocuments = documents.data.sort((a: Document, b: Document) => { + const dateA = new Date(a.created as string); + const dateB = new Date(b.created as string); + return dateA.getTime() - dateB.getTime(); + }); + set(documentsAtom, sortedDocuments); + } catch (error) { + set(documentsErrorAtom, 'Failed to fetch documents'); + } finally { + set(documentsLoadingAtom, false); + } + } +); + +export const selectedDocumentAtom = atom({ name: '', description: '', labels: {}, owner_id: '', project_id: '', path: '' }); diff --git a/ui/src/components/feature/AddEditDocumentModal.tsx b/ui/src/components/feature/AddEditDocumentModal.tsx new file mode 100644 index 0000000..ec1fdcb --- /dev/null +++ b/ui/src/components/feature/AddEditDocumentModal.tsx @@ -0,0 +1,102 @@ +// Copyright 2024 Iguazio +// +// 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 { publicUserAtom } from '@atoms/index' +import { + Button, + FormControl, + FormLabel, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay +} from '@chakra-ui/react' +import { Document } from '@shared/types/document' +import { useAtom } from 'jotai' +import React, { useEffect, useState } from 'react' + +type DocumentModalProps = { + isOpen: boolean + onClose: () => void + onSave: (document: Document) => void + document?: Document +} + +const AddEditDocumentModal: React.FC = ({ isOpen, onClose, onSave, document }) => { + const [publicUser] = useAtom(publicUserAtom) + const [formData, setFormData] = useState( + document || { + name: '', + description: '', + owner_id: publicUser.uid as string, + project_id: '', + path: '' + } + ) + useEffect(() => { + if (document) { + setFormData(document) + } + }, [document]) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData({ ...formData, [name]: value }) + } + + const handleSubmit = () => { + onSave(formData) + onClose() + } + + return ( + + + + + {document?.uid ? 'Edit' : 'Add New'} {' Document'} + + + + + Document Name + + + + Description + + + + Path + + + + + + + + + + ) +} + +export default AddEditDocumentModal diff --git a/ui/src/components/feature/DocumentsTable.tsx b/ui/src/components/feature/DocumentsTable.tsx new file mode 100644 index 0000000..072f634 --- /dev/null +++ b/ui/src/components/feature/DocumentsTable.tsx @@ -0,0 +1,222 @@ +// Copyright 2024 Iguazio +// +// 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 { documentsAtom, documentsWithFetchAtom } from '@atoms/documents' +import { selectedRowAtom } from '@atoms/index' +import { AddIcon, DeleteIcon } from '@chakra-ui/icons' +import { + Button, + Drawer, + DrawerBody, + DrawerContent, + DrawerHeader, + Flex, + FormControl, + FormLabel, + Input, + useDisclosure, + useToast +} from '@chakra-ui/react' +import Breadcrumbs from '@components/shared/Breadcrumbs' +import DataTableComponent from '@components/shared/Datatable' +import FilterComponent from '@components/shared/Filter' +import Client from '@services/Api' +import { Document } from '@shared/types/document' +import { useAtom } from 'jotai' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { TableColumn } from 'react-data-table-component' +import AddEditDocumentModal from './AddEditDocumentModal' + +const DocumentsTable: React.FC = () => { + const [selectedRow, setSelectedRow] = useAtom(selectedRowAtom) + const [documents] = useAtom(documentsAtom) + + const [selectedRows, setSelectedRows] = useState([]) + const [editRow, setEditRow] = useState({ + name: '', + description: '', + owner_id: '', + project_id: '', + path: '' + }) + const [filterText, setFilterText] = useState('') + const [toggledClearRows, setToggleClearRows] = useState(false) + + const [columns] = useState([ + { name: 'name', selector: (row: Partial) => row.name ?? '', sortable: true }, + { name: 'description', selector: (row: Partial) => row.description ?? '', sortable: true }, + { name: 'version', selector: (row: Partial) => row.version ?? '', sortable: true } + ]) + + const [, fetchDocuments] = useAtom(documentsWithFetchAtom) + + const { isOpen: isModalOpen, onOpen: onModalOpen, onClose: onModalClose } = useDisclosure() + const { isOpen: isDrawerOpen, onOpen: onDrawerOpen, onClose: onDrawerClose } = useDisclosure() + + const toast = useToast() + + useEffect(() => { + fetchDocuments('default') + }, [fetchDocuments]) + + const handleSave = async (document: Document) => { + try { + if (document.uid) { + await Client.updateDocument('default', document) + toast({ title: 'Document updated.', status: 'success', duration: 3000, isClosable: true }) + } else { + await Client.createDocument('default', document) + toast({ title: 'Document added successfully.', status: 'success', duration: 3000, isClosable: true }) + } + await fetchDocuments('default') + onDrawerClose() + } catch (error) { + console.error('Error saving document:', error) + toast({ title: 'Error saving document.', status: 'error', duration: 3000, isClosable: true }) + } + } + + const handleClearRows = useCallback(() => { + setToggleClearRows(!toggledClearRows) + }, [toggledClearRows]) + + const handleDelete = useCallback(async () => { + try { + await Promise.all(selectedRows.map(row => Client.deleteDocument('default', row.name as string))) + setSelectedRows([]) + await fetchDocuments('default') + toast({ title: 'Documents deleted.', status: 'success', duration: 3000, isClosable: true }) + } catch (error) { + console.error('Error deleting documents:', error) + toast({ title: 'Error deleting documents.', status: 'error', duration: 3000, isClosable: true }) + } + handleClearRows() + }, [fetchDocuments, selectedRows, toast, handleClearRows]) + + const handleUpdate = async () => { + try { + await Client.updateDocument('default', selectedRow) + toast({ + title: 'Document updated.', + description: 'The document has been updated successfully.', + status: 'success', + duration: 3000, + isClosable: true + }) + await fetchDocuments('default') + onDrawerClose() + } catch (error) { + toast({ + title: 'Error updating document.', + description: 'There was an error updating the document.', + status: 'error', + duration: 3000, + isClosable: true + }) + } + } + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setSelectedRow({ ...selectedRow, [name]: value }) + } + + const contextActions = useMemo( + () => ( + + ), + [handleDelete] + ) + + const subHeaderComponentMemo = useMemo( + () => ( + + setFilterText(e.target.value)} filterText={filterText} /> + + + ), + [filterText] + ) + + return ( + + + >[]} + contextActions={contextActions} + onSelectedRowChange={e => { + setSelectedRows(e.selectedRows) + }} + subheaderComponent={subHeaderComponentMemo} + filterText={filterText} + onOpenDrawer={() => { + onDrawerOpen() + }} + toggleClearRows={toggledClearRows} + /> + + + + {selectedRow?.name} + + + + + Name + + + + Description + + + + Path + + + + + + + + + + ) +} + +export default DocumentsTable diff --git a/ui/src/components/feature/Layout.tsx b/ui/src/components/feature/Layout.tsx index 2de57c4..73cf8c2 100644 --- a/ui/src/components/feature/Layout.tsx +++ b/ui/src/components/feature/Layout.tsx @@ -60,6 +60,7 @@ const Layout: React.FC = ({ children }) => { navigate('/admin/data-sources')}>Data Sources navigate('/admin/datasets')}>Datasets navigate('/admin/models')}>Models + navigate('/admin/documents')}>Documents )} diff --git a/ui/src/pages/DocumentsTablePage.tsx b/ui/src/pages/DocumentsTablePage.tsx new file mode 100644 index 0000000..6cc03e9 --- /dev/null +++ b/ui/src/pages/DocumentsTablePage.tsx @@ -0,0 +1,24 @@ +// Copyright 2024 Iguazio +// +// 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 DocumentsTable from '@components/feature/DocumentsTable' +import Layout from '@components/feature/Layout' + +export const DocumentsTablePage = () => { + return ( + + + + ) +} diff --git a/ui/src/services/Api.ts b/ui/src/services/Api.ts index 4ba2115..453a0fb 100644 --- a/ui/src/services/Api.ts +++ b/ui/src/services/Api.ts @@ -15,6 +15,7 @@ import { User } from '@shared/types'; import { DataSource } from '@shared/types/dataSource'; import { Dataset } from '@shared/types/dataset'; +import { Document } from '@shared/types/document'; import { Model } from '@shared/types/model'; import { Project } from '@shared/types/project'; import { Session } from '@shared/types/session'; @@ -369,8 +370,53 @@ class ApiClient { return this.handleError(error); } } -} + // DOCUMENTS + + async getDocuments(projectName: string, params?: { name?: string; version?: string; labels?: string[]; mode?: string }) { + try { + const response = await this.client.get(`/projects/${projectName}/documents`, { params }); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + async getDocument(projectName: string, uid: string) { + try { + const response = await this.client.get(`/projects/${projectName}/documents/${uid}`); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + async createDocument(projectName: string, document: Document) { + try { + const response = await this.client.post(`/projects/${projectName}/documents`, document); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + async updateDocument(projectName: string, document: Document) { + try { + const response = await this.client.put(`/projects/${projectName}/documents/${document.name}`, document); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + async deleteDocument(projectName: string, uid: string) { + try { + const response = await this.client.delete(`/projects/${projectName}/documents/${uid}`); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } +} function getClient() { return new ApiClient() diff --git a/ui/src/shared/types/document.ts b/ui/src/shared/types/document.ts new file mode 100644 index 0000000..467e456 --- /dev/null +++ b/ui/src/shared/types/document.ts @@ -0,0 +1,26 @@ +// Copyright 2024 Iguazio +// +// 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. + +export type Document = { + name: string + uid?: string + description?: string + labels?: { [key: string]: string } + owner_id: string + version?: string + project_id: string + path: string + origin?: string + created?: string +} From 6e3d1f9d77b931ed6ca69f8906a8d547d81d4135 Mon Sep 17 00:00:00 2001 From: baaalint Date: Wed, 11 Sep 2024 12:04:33 +0200 Subject: [PATCH 2/4] feat(admin): add prompt templates table --- ui/src/App.tsx | 2 + ui/src/atoms/promptTemplates.ts | 47 ++++ .../feature/AddEditPromptTemplateModal.tsx | 108 +++++++++ ui/src/components/feature/Layout.tsx | 1 + .../feature/PromptTemplatesTable.tsx | 229 ++++++++++++++++++ ui/src/pages/PromptTemplatesTablePage.tsx | 24 ++ ui/src/services/Api.ts | 50 ++++ ui/src/shared/types/promptTemplate.ts | 29 +++ 8 files changed, 490 insertions(+) create mode 100644 ui/src/atoms/promptTemplates.ts create mode 100644 ui/src/components/feature/AddEditPromptTemplateModal.tsx create mode 100644 ui/src/components/feature/PromptTemplatesTable.tsx create mode 100644 ui/src/pages/PromptTemplatesTablePage.tsx create mode 100644 ui/src/shared/types/promptTemplate.ts diff --git a/ui/src/App.tsx b/ui/src/App.tsx index f3964f3..cdb06ec 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -23,6 +23,7 @@ import { DocumentsTablePage } from 'pages/DocumentsTablePage' import { LoginPage } from 'pages/LoginPage' import { ModelsTablePage } from 'pages/ModelsTablePage' import { ProjectsTablePage } from 'pages/ProjectsTablePage' +import { PromptTemplatesTablePage } from 'pages/PromptTemplatesTablePage' import { UsersTablePage } from 'pages/UsersTablePage' import { RouterProvider, createBrowserRouter } from 'react-router-dom' @@ -43,6 +44,7 @@ function App() { { path: '/admin/datasets', element: }, { path: '/admin/models', element: }, { path: '/admin/documents', element: }, + { path: '/admin/prompt-templates', element: }, { path: '/admin/histories', element: diff --git a/ui/src/atoms/promptTemplates.ts b/ui/src/atoms/promptTemplates.ts new file mode 100644 index 0000000..a331670 --- /dev/null +++ b/ui/src/atoms/promptTemplates.ts @@ -0,0 +1,47 @@ +// Copyright 2024 Iguazio +// +// 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 Client from '@services/Api'; +import { PromptTemplate } from '@shared/types/promptTemplate'; +import { atom } from 'jotai'; + +export const promptTemplatesAtom = atom([]); + +export const promptTemplatesLoadingAtom = atom(false); + +export const promptTemplatesErrorAtom = atom(null); + + +export const promptTemplatesWithFetchAtom = atom( + (get) => get(promptTemplatesAtom), + async (_get, set, username) => { + set(promptTemplatesLoadingAtom, true); + set(promptTemplatesErrorAtom, null); + try { + const promptTemplates = await Client.getPromptTemplates(username as string); + const sortedPromptTemplates = promptTemplates.data.sort((a: PromptTemplate, b: PromptTemplate) => { + const dateA = new Date(a.created as string); + const dateB = new Date(b.created as string); + return dateA.getTime() - dateB.getTime(); + }); + set(promptTemplatesAtom, sortedPromptTemplates); + } catch (error) { + set(promptTemplatesErrorAtom, 'Failed to fetch promptTemplates'); + } finally { + set(promptTemplatesLoadingAtom, false); + } + } +); + +export const selectedPromptTemplateAtom = atom({ name: '', description: '', labels: {}, owner_id: '', project_id: '', text: '', arguments: [] }); diff --git a/ui/src/components/feature/AddEditPromptTemplateModal.tsx b/ui/src/components/feature/AddEditPromptTemplateModal.tsx new file mode 100644 index 0000000..84de6f6 --- /dev/null +++ b/ui/src/components/feature/AddEditPromptTemplateModal.tsx @@ -0,0 +1,108 @@ +// Copyright 2024 Iguazio +// +// 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 { publicUserAtom } from '@atoms/index' +import { + Button, + FormControl, + FormLabel, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay +} from '@chakra-ui/react' +import { PromptTemplate } from '@shared/types/promptTemplate' +import { useAtom } from 'jotai' +import React, { useEffect, useState } from 'react' + +type PromptTemplateModalProps = { + isOpen: boolean + onClose: () => void + onSave: (promptTemplate: PromptTemplate) => void + promptTemplate?: PromptTemplate +} + +const AddEditPromptTemplateModal: React.FC = ({ + isOpen, + onClose, + onSave, + promptTemplate +}) => { + const [publicUser] = useAtom(publicUserAtom) + const [formData, setFormData] = useState( + promptTemplate || { + name: '', + description: '', + owner_id: publicUser.uid as string, + project_id: '', + text: '', + arguments: ['prompt'] + } + ) + useEffect(() => { + if (promptTemplate) { + setFormData(promptTemplate) + } + }, [promptTemplate]) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData({ ...formData, [name]: value }) + } + + const handleSubmit = () => { + onSave(formData) + onClose() + } + + return ( + + + + + {promptTemplate?.uid ? 'Edit' : 'Add New'} {' PromptTemplate'} + + + + + PromptTemplate Name + + + + Description + + + + Text + + + + + + + + + + ) +} + +export default AddEditPromptTemplateModal diff --git a/ui/src/components/feature/Layout.tsx b/ui/src/components/feature/Layout.tsx index 73cf8c2..dc93f0d 100644 --- a/ui/src/components/feature/Layout.tsx +++ b/ui/src/components/feature/Layout.tsx @@ -61,6 +61,7 @@ const Layout: React.FC = ({ children }) => { navigate('/admin/datasets')}>Datasets navigate('/admin/models')}>Models navigate('/admin/documents')}>Documents + navigate('/admin/prompt-templates')}>Prompt Templates )} diff --git a/ui/src/components/feature/PromptTemplatesTable.tsx b/ui/src/components/feature/PromptTemplatesTable.tsx new file mode 100644 index 0000000..d72db8c --- /dev/null +++ b/ui/src/components/feature/PromptTemplatesTable.tsx @@ -0,0 +1,229 @@ +// Copyright 2024 Iguazio +// +// 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 { selectedRowAtom } from '@atoms/index' +import { promptTemplatesAtom, promptTemplatesWithFetchAtom } from '@atoms/promptTemplates' +import { AddIcon, DeleteIcon } from '@chakra-ui/icons' +import { + Button, + Drawer, + DrawerBody, + DrawerContent, + DrawerHeader, + Flex, + FormControl, + FormLabel, + Input, + useDisclosure, + useToast +} from '@chakra-ui/react' +import Breadcrumbs from '@components/shared/Breadcrumbs' +import DataTableComponent from '@components/shared/Datatable' +import FilterComponent from '@components/shared/Filter' +import Client from '@services/Api' +import { PromptTemplate } from '@shared/types/promptTemplate' +import { useAtom } from 'jotai' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { TableColumn } from 'react-data-table-component' +import AddEditPromptTemplateModal from './AddEditPromptTemplateModal' + +const PromptTemplatesTable: React.FC = () => { + const [selectedRow, setSelectedRow] = useAtom(selectedRowAtom) + const [promptTemplates] = useAtom(promptTemplatesAtom) + + const [selectedRows, setSelectedRows] = useState([]) + const [editRow, setEditRow] = useState({ + name: '', + description: '', + owner_id: '', + project_id: '', + text: '', + arguments: [] + }) + const [filterText, setFilterText] = useState('') + const [toggledClearRows, setToggleClearRows] = useState(false) + + const [columns] = useState([ + { name: 'name', selector: (row: Partial) => row.name ?? '', sortable: true }, + { name: 'description', selector: (row: Partial) => row.description ?? '', sortable: true }, + { name: 'version', selector: (row: Partial) => row.version ?? '', sortable: true } + ]) + + const [, fetchPromptTemplates] = useAtom(promptTemplatesWithFetchAtom) + + const { isOpen: isModalOpen, onOpen: onModalOpen, onClose: onModalClose } = useDisclosure() + const { isOpen: isDrawerOpen, onOpen: onDrawerOpen, onClose: onDrawerClose } = useDisclosure() + + const toast = useToast() + + useEffect(() => { + fetchPromptTemplates('default') + }, [fetchPromptTemplates]) + + const handleSave = async (promptTemplate: PromptTemplate) => { + try { + if (promptTemplate.uid) { + await Client.updatePromptTemplate('default', promptTemplate) + toast({ title: 'PromptTemplate updated.', status: 'success', duration: 3000, isClosable: true }) + } else { + await Client.createPromptTemplate('default', promptTemplate) + toast({ title: 'PromptTemplate added successfully.', status: 'success', duration: 3000, isClosable: true }) + } + await fetchPromptTemplates('default') + onDrawerClose() + } catch (error) { + console.error('Error saving promptTemplate:', error) + toast({ title: 'Error saving promptTemplate.', status: 'error', duration: 3000, isClosable: true }) + } + } + + const handleClearRows = useCallback(() => { + setToggleClearRows(!toggledClearRows) + }, [toggledClearRows]) + + const handleDelete = useCallback(async () => { + try { + await Promise.all(selectedRows.map(row => Client.deletePromptTemplate('default', row.name as string))) + setSelectedRows([]) + await fetchPromptTemplates('default') + toast({ title: 'PromptTemplates deleted.', status: 'success', duration: 3000, isClosable: true }) + } catch (error) { + console.error('Error deleting promptTemplates:', error) + toast({ title: 'Error deleting promptTemplates.', status: 'error', duration: 3000, isClosable: true }) + } + handleClearRows() + }, [fetchPromptTemplates, selectedRows, toast, handleClearRows]) + + const handleUpdate = async () => { + try { + await Client.updatePromptTemplate('default', selectedRow) + toast({ + title: 'PromptTemplate updated.', + description: 'The promptTemplate has been updated successfully.', + status: 'success', + duration: 3000, + isClosable: true + }) + await fetchPromptTemplates('default') + onDrawerClose() + } catch (error) { + toast({ + title: 'Error updating promptTemplate.', + description: 'There was an error updating the promptTemplate.', + status: 'error', + duration: 3000, + isClosable: true + }) + } + } + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setSelectedRow({ ...selectedRow, [name]: value }) + } + + const contextActions = useMemo( + () => ( + + ), + [handleDelete] + ) + + const subHeaderComponentMemo = useMemo( + () => ( + + setFilterText(e.target.value)} filterText={filterText} /> + + + ), + [filterText] + ) + + return ( + + + >[]} + contextActions={contextActions} + onSelectedRowChange={e => { + setSelectedRows(e.selectedRows) + }} + subheaderComponent={subHeaderComponentMemo} + filterText={filterText} + onOpenDrawer={() => { + onDrawerOpen() + }} + toggleClearRows={toggledClearRows} + /> + + + + {selectedRow?.name} + + + + + Name + + + + Description + + + + Path + + + + + + + + + + ) +} + +export default PromptTemplatesTable diff --git a/ui/src/pages/PromptTemplatesTablePage.tsx b/ui/src/pages/PromptTemplatesTablePage.tsx new file mode 100644 index 0000000..1fc8b4d --- /dev/null +++ b/ui/src/pages/PromptTemplatesTablePage.tsx @@ -0,0 +1,24 @@ +// Copyright 2024 Iguazio +// +// 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 Layout from '@components/feature/Layout' +import PromptTemplatesTable from '@components/feature/PromptTemplatesTable' + +export const PromptTemplatesTablePage = () => { + return ( + + + + ) +} diff --git a/ui/src/services/Api.ts b/ui/src/services/Api.ts index 453a0fb..dc0d09f 100644 --- a/ui/src/services/Api.ts +++ b/ui/src/services/Api.ts @@ -18,6 +18,7 @@ import { Dataset } from '@shared/types/dataset'; import { Document } from '@shared/types/document'; import { Model } from '@shared/types/model'; import { Project } from '@shared/types/project'; +import { PromptTemplate } from '@shared/types/promptTemplate'; import { Session } from '@shared/types/session'; import { Query } from '@shared/types/workflow'; import axios, { AxiosResponse } from 'axios'; @@ -416,6 +417,55 @@ class ApiClient { return this.handleError(error); } } + + // PROMPT TEMPLATES + + async getPromptTemplates(projectName: string, params?: { name?: string; version?: string; labels?: string[]; mode?: string }) { + try { + const response = await this.client.get(`/projects/${projectName}/prompt_templates`, { params }); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + async getPromptTemplate(projectName: string, uid: string) { + try { + const response = await this.client.get(`/projects/${projectName}/prompt_templates/${uid}`); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + async createPromptTemplate(projectName: string, promptTemplate: PromptTemplate) { + try { + const response = await this.client.post(`/projects/${projectName}/prompt_templates`, promptTemplate); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + async updatePromptTemplate(projectName: string, promptTemplate: PromptTemplate) { + try { + const response = await this.client.put(`/projects/${projectName}/prompt_templates/${promptTemplate.name}`, promptTemplate); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + async deletePromptTemplate(projectName: string, uid: string) { + try { + const response = await this.client.delete(`/projects/${projectName}/prompt_templates/${uid}`); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + } function getClient() { diff --git a/ui/src/shared/types/promptTemplate.ts b/ui/src/shared/types/promptTemplate.ts new file mode 100644 index 0000000..64fd015 --- /dev/null +++ b/ui/src/shared/types/promptTemplate.ts @@ -0,0 +1,29 @@ +// Copyright 2024 Iguazio +// +// 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. + + + + +export type PromptTemplate = { + name: string + uid?: string + description?: string + labels?: { [key: string]: string } + owner_id: string + version?: string + project_id: string + text: string + arguments: string[] + created?: string +} From 32262d4754bcb5d3ff0178fec4375db42506126f Mon Sep 17 00:00:00 2001 From: baaalint Date: Wed, 11 Sep 2024 12:30:16 +0200 Subject: [PATCH 3/4] feat(admin): add workflows table --- ui/src/atoms/workflows.ts | 47 ++++ .../feature/AddEditWorkflowModal.tsx | 103 ++++++++ ui/src/components/feature/Layout.tsx | 1 + ui/src/components/feature/WorkflowsTable.tsx | 224 ++++++++++++++++++ ui/src/pages/WorkflowsTablePage.tsx | 24 ++ ui/src/services/Api.ts | 50 +++- ui/src/shared/types/workflow.ts | 15 +- 7 files changed, 449 insertions(+), 15 deletions(-) create mode 100644 ui/src/atoms/workflows.ts create mode 100644 ui/src/components/feature/AddEditWorkflowModal.tsx create mode 100644 ui/src/components/feature/WorkflowsTable.tsx create mode 100644 ui/src/pages/WorkflowsTablePage.tsx diff --git a/ui/src/atoms/workflows.ts b/ui/src/atoms/workflows.ts new file mode 100644 index 0000000..f1dd9a1 --- /dev/null +++ b/ui/src/atoms/workflows.ts @@ -0,0 +1,47 @@ +// Copyright 2024 Iguazio +// +// 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 Client from '@services/Api'; +import { Workflow, WorkflowType } from '@shared/types/workflow'; +import { atom } from 'jotai'; + +export const workflowsAtom = atom([]); + +export const workflowsLoadingAtom = atom(false); + +export const workflowsErrorAtom = atom(null); + + +export const workflowsWithFetchAtom = atom( + (get) => get(workflowsAtom), + async (_get, set, username) => { + set(workflowsLoadingAtom, true); + set(workflowsErrorAtom, null); + try { + const workflows = await Client.getWorkflows(username as string); + const sortedWorkflows = workflows.data.sort((a: Workflow, b: Workflow) => { + const dateA = new Date(a.created as string); + const dateB = new Date(b.created as string); + return dateA.getTime() - dateB.getTime(); + }); + set(workflowsAtom, sortedWorkflows); + } catch (error) { + set(workflowsErrorAtom, 'Failed to fetch workflows'); + } finally { + set(workflowsLoadingAtom, false); + } + } +); + +export const selectedWorkflowAtom = atom({ name: '', description: '', labels: {}, owner_id: '', project_id: '', workflow_type: WorkflowType.APPLICATION, deployment: '' }); diff --git a/ui/src/components/feature/AddEditWorkflowModal.tsx b/ui/src/components/feature/AddEditWorkflowModal.tsx new file mode 100644 index 0000000..9167336 --- /dev/null +++ b/ui/src/components/feature/AddEditWorkflowModal.tsx @@ -0,0 +1,103 @@ +// Copyright 2024 Iguazio +// +// 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 { publicUserAtom } from '@atoms/index' +import { + Button, + FormControl, + FormLabel, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay +} from '@chakra-ui/react' +import { Workflow, WorkflowType } from '@shared/types/workflow' +import { useAtom } from 'jotai' +import React, { useEffect, useState } from 'react' + +type WorkflowModalProps = { + isOpen: boolean + onClose: () => void + onSave: (workflow: Workflow) => void + workflow?: Workflow +} + +const AddEditWorkflowModal: React.FC = ({ isOpen, onClose, onSave, workflow }) => { + const [publicUser] = useAtom(publicUserAtom) + const [formData, setFormData] = useState( + workflow || { + name: '', + description: '', + owner_id: publicUser.uid as string, + project_id: '', + workflow_type: WorkflowType.APPLICATION, + deployment: '' + } + ) + useEffect(() => { + if (workflow) { + setFormData(workflow) + } + }, [workflow]) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData({ ...formData, [name]: value }) + } + + const handleSubmit = () => { + onSave(formData) + onClose() + } + + return ( + + + + + {workflow?.uid ? 'Edit' : 'Add New'} {' Workflow'} + + + + + Workflow Name + + + + Description + + + + Deployment + + + + + + + + + + ) +} + +export default AddEditWorkflowModal diff --git a/ui/src/components/feature/Layout.tsx b/ui/src/components/feature/Layout.tsx index dc93f0d..29214e8 100644 --- a/ui/src/components/feature/Layout.tsx +++ b/ui/src/components/feature/Layout.tsx @@ -62,6 +62,7 @@ const Layout: React.FC = ({ children }) => { navigate('/admin/models')}>Models navigate('/admin/documents')}>Documents navigate('/admin/prompt-templates')}>Prompt Templates + navigate('/admin/workflows')}>Workflows )} diff --git a/ui/src/components/feature/WorkflowsTable.tsx b/ui/src/components/feature/WorkflowsTable.tsx new file mode 100644 index 0000000..c94369b --- /dev/null +++ b/ui/src/components/feature/WorkflowsTable.tsx @@ -0,0 +1,224 @@ +// Copyright 2024 Iguazio +// +// 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 { selectedRowAtom } from '@atoms/index' +import { workflowsAtom, workflowsWithFetchAtom } from '@atoms/workflows' +import { AddIcon, DeleteIcon } from '@chakra-ui/icons' +import { + Button, + Drawer, + DrawerBody, + DrawerContent, + DrawerHeader, + Flex, + FormControl, + FormLabel, + Input, + useDisclosure, + useToast +} from '@chakra-ui/react' +import Breadcrumbs from '@components/shared/Breadcrumbs' +import DataTableComponent from '@components/shared/Datatable' +import FilterComponent from '@components/shared/Filter' +import Client from '@services/Api' +import { Workflow, WorkflowType } from '@shared/types/workflow' +import { useAtom } from 'jotai' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { TableColumn } from 'react-data-table-component' +import AddEditWorkflowModal from './AddEditWorkflowModal' + +const WorkflowsTable: React.FC = () => { + const [selectedRow, setSelectedRow] = useAtom(selectedRowAtom) + const [workflows] = useAtom(workflowsAtom) + + const [selectedRows, setSelectedRows] = useState([]) + const [editRow, setEditRow] = useState({ + name: '', + description: '', + owner_id: '', + project_id: '', + workflow_type: WorkflowType.DEPLOYMENT, + deployment: '' + }) + const [filterText, setFilterText] = useState('') + const [toggledClearRows, setToggleClearRows] = useState(false) + + const [columns] = useState([ + { name: 'name', selector: (row: Partial) => row.name ?? '', sortable: true }, + { name: 'description', selector: (row: Partial) => row.description ?? '', sortable: true }, + { name: 'version', selector: (row: Partial) => row.version ?? '', sortable: true } + ]) + + const [, fetchWorkflows] = useAtom(workflowsWithFetchAtom) + + const { isOpen: isModalOpen, onOpen: onModalOpen, onClose: onModalClose } = useDisclosure() + const { isOpen: isDrawerOpen, onOpen: onDrawerOpen, onClose: onDrawerClose } = useDisclosure() + + const toast = useToast() + + useEffect(() => { + fetchWorkflows('default') + }, [fetchWorkflows]) + + const handleSave = async (workflow: Workflow) => { + try { + if (workflow.uid) { + await Client.updateWorkflow('default', workflow) + toast({ title: 'Workflow updated.', status: 'success', duration: 3000, isClosable: true }) + } else { + await Client.createWorkflow('default', workflow) + toast({ title: 'Workflow added successfully.', status: 'success', duration: 3000, isClosable: true }) + } + await fetchWorkflows('default') + onDrawerClose() + } catch (error) { + console.error('Error saving workflow:', error) + toast({ title: 'Error saving workflow.', status: 'error', duration: 3000, isClosable: true }) + } + } + + const handleClearRows = useCallback(() => { + setToggleClearRows(!toggledClearRows) + }, [toggledClearRows]) + + const handleDelete = useCallback(async () => { + try { + await Promise.all(selectedRows.map(row => Client.deleteWorkflow('default', row.name as string))) + setSelectedRows([]) + await fetchWorkflows('default') + toast({ title: 'Workflows deleted.', status: 'success', duration: 3000, isClosable: true }) + } catch (error) { + console.error('Error deleting workflows:', error) + toast({ title: 'Error deleting workflows.', status: 'error', duration: 3000, isClosable: true }) + } + handleClearRows() + }, [fetchWorkflows, selectedRows, toast, handleClearRows]) + + const handleUpdate = async () => { + try { + await Client.updateWorkflow('default', selectedRow) + toast({ + title: 'Workflow updated.', + description: 'The workflow has been updated successfully.', + status: 'success', + duration: 3000, + isClosable: true + }) + await fetchWorkflows('default') + onDrawerClose() + } catch (error) { + toast({ + title: 'Error updating workflow.', + description: 'There was an error updating the workflow.', + status: 'error', + duration: 3000, + isClosable: true + }) + } + } + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setSelectedRow({ ...selectedRow, [name]: value }) + } + + const contextActions = useMemo( + () => ( + + ), + [handleDelete] + ) + + const subHeaderComponentMemo = useMemo( + () => ( + + setFilterText(e.target.value)} filterText={filterText} /> + + + ), + [filterText] + ) + + return ( + + + >[]} + contextActions={contextActions} + onSelectedRowChange={e => { + setSelectedRows(e.selectedRows) + }} + subheaderComponent={subHeaderComponentMemo} + filterText={filterText} + onOpenDrawer={() => { + onDrawerOpen() + }} + toggleClearRows={toggledClearRows} + /> + + + + {selectedRow?.name} + + + + + Name + + + + Description + + + + Path + + + + + + + + + + ) +} + +export default WorkflowsTable diff --git a/ui/src/pages/WorkflowsTablePage.tsx b/ui/src/pages/WorkflowsTablePage.tsx new file mode 100644 index 0000000..ea398eb --- /dev/null +++ b/ui/src/pages/WorkflowsTablePage.tsx @@ -0,0 +1,24 @@ +// Copyright 2024 Iguazio +// +// 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 Layout from '@components/feature/Layout' +import WorkflowsTable from '@components/feature/WorkflowsTable' + +export const WorkflowsTablePage = () => { + return ( + + + + ) +} diff --git a/ui/src/services/Api.ts b/ui/src/services/Api.ts index dc0d09f..ffb3e2e 100644 --- a/ui/src/services/Api.ts +++ b/ui/src/services/Api.ts @@ -20,7 +20,7 @@ import { Model } from '@shared/types/model'; import { Project } from '@shared/types/project'; import { PromptTemplate } from '@shared/types/promptTemplate'; import { Session } from '@shared/types/session'; -import { Query } from '@shared/types/workflow'; +import { Query, Workflow } from '@shared/types/workflow'; import axios, { AxiosResponse } from 'axios'; @@ -148,15 +148,49 @@ class ApiClient { // WORKFLOWS - async getWorkflow(projectId: string, workflowId: string, query: Query) { + + async getWorkflows(projectName: string, params?: { name?: string; version?: string; workflow_type?: string; labels?: string[]; mode?: string }) { try { - const response = await this.client.post( - `/projects/${projectId}/workflows/${workflowId}`, // is it project ID or name? - query - ) - return this.handleResponse(response) + const response = await this.client.get(`/projects/${projectName}/workflows`, { params }); + return this.handleResponse(response); } catch (error) { - return this.handleError(error as Error) + return this.handleError(error); + } + } + + async getWorkflow(projectName: string, uid: string) { + try { + const response = await this.client.get(`/projects/${projectName}/workflows/${uid}`); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + async createWorkflow(projectName: string, workflow: Workflow) { + try { + const response = await this.client.post(`/projects/${projectName}/workflows`, workflow); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + async updateWorkflow(projectName: string, workflow: Workflow) { + try { + const response = await this.client.put(`/projects/${projectName}/workflows/${workflow.name}`, workflow); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); + } + } + + async deleteWorkflow(projectName: string, uid: string) { + try { + const response = await this.client.delete(`/projects/${projectName}/workflows/${uid}`); + return this.handleResponse(response); + } catch (error) { + return this.handleError(error); } } diff --git a/ui/src/shared/types/workflow.ts b/ui/src/shared/types/workflow.ts index 0ec9cbc..55def0e 100644 --- a/ui/src/shared/types/workflow.ts +++ b/ui/src/shared/types/workflow.ts @@ -14,17 +14,18 @@ export type Workflow = { name: string - uid: string - description: string - labels: { [key: string]: string } + uid?: string + description?: string + labels?: { [key: string]: string } owner_id: string - version: string + version?: string project_id: string workflow_type: WorkflowType deployment: string - workflow_function: string - configuration: { [key: string]: string } - graph: { [key: string]: string } + workflow_function?: string + configuration?: { [key: string]: string } + graph?: { [key: string]: string } + created?: string } From 123b876cb57c313dc4fd6b4d994b2df37de64662 Mon Sep 17 00:00:00 2001 From: baaalint Date: Thu, 12 Sep 2024 13:58:43 +0200 Subject: [PATCH 4/4] feat(admin): add workflows table to routes --- ui/src/App.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index cdb06ec..6e417c3 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -25,6 +25,7 @@ import { ModelsTablePage } from 'pages/ModelsTablePage' import { ProjectsTablePage } from 'pages/ProjectsTablePage' import { PromptTemplatesTablePage } from 'pages/PromptTemplatesTablePage' import { UsersTablePage } from 'pages/UsersTablePage' +import { WorkflowsTablePage } from 'pages/WorkflowsTablePage' import { RouterProvider, createBrowserRouter } from 'react-router-dom' function App() { @@ -45,6 +46,7 @@ function App() { { path: '/admin/models', element: }, { path: '/admin/documents', element: }, { path: '/admin/prompt-templates', element: }, + { path: '/admin/workflows', element: }, { path: '/admin/histories', element: