diff --git a/frontend/src/components/custom-tools/add-custom-tool-form-modal/AddCustomToolFormModal.jsx b/frontend/src/components/custom-tools/add-custom-tool-form-modal/AddCustomToolFormModal.jsx
index 12401fd17..f8b325d74 100644
--- a/frontend/src/components/custom-tools/add-custom-tool-form-modal/AddCustomToolFormModal.jsx
+++ b/frontend/src/components/custom-tools/add-custom-tool-form-modal/AddCustomToolFormModal.jsx
@@ -73,7 +73,9 @@ function AddCustomToolFormModal({
});
setOpen(false);
clearFormDetails();
- navigate(success?.tool_id);
+ if (!isEdit) {
+ navigate(success?.tool_id);
+ }
})
.catch((err) => {
handleException(err, "", setBackendErrors);
diff --git a/frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx b/frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx
index f610b7b5d..838440405 100644
--- a/frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx
+++ b/frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx
@@ -1,335 +1,115 @@
import { PlusOutlined } from "@ant-design/icons";
-import { useEffect, useState } from "react";
-
-import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate";
-import { useAlertStore } from "../../../store/alert-store";
-import { useSessionStore } from "../../../store/session-store";
-import { CustomButton } from "../../widgets/custom-button/CustomButton";
-import { AddCustomToolFormModal } from "../add-custom-tool-form-modal/AddCustomToolFormModal";
-import { ViewTools } from "../view-tools/ViewTools";
-import "./ListOfTools.css";
-import { useExceptionHandler } from "../../../hooks/useExceptionHandler";
-import { ToolNavBar } from "../../navigations/tool-nav-bar/ToolNavBar";
-import { SharePermission } from "../../widgets/share-permission/SharePermission";
-import usePostHogEvents from "../../../hooks/usePostHogEvents.js";
-
-let OnboardMessagesModal;
-let slides;
-try {
- OnboardMessagesModal =
- require("../../../plugins/onboarding-messages/OnboardMessagesModal.jsx").OnboardMessagesModal;
- slides =
- require("../../../plugins/onboarding-messages/login-slides.jsx").LoginSlides;
-} catch (err) {
- OnboardMessagesModal = null;
- slides = [];
-}
+import { useCallback, useMemo } from "react";
+import usePostHogEvents from "../../../hooks/usePostHogEvents";
+import { useListManager } from "../../../hooks/useListManager";
+import ListOfToolsModal from "./ListOfToolsModal";
+import { ListView } from "../../view-projects/ListView";
function ListOfTools() {
- const [isListLoading, setIsListLoading] = useState(false);
- const [openAddTool, setOpenAddTool] = useState(false);
- const [editItem, setEditItem] = useState(null);
- const { sessionDetails } = useSessionStore();
- const { loginOnboardingMessage } = sessionDetails;
const { setPostHogCustomEvent } = usePostHogEvents();
- const { setAlertDetails } = useAlertStore();
- const axiosPrivate = useAxiosPrivate();
+ const getBaseUrl = (sessionDetails) =>
+ `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/`;
- const [listOfTools, setListOfTools] = useState([]);
- const [filteredListOfTools, setFilteredListOfTools] = useState([]);
- const handleException = useExceptionHandler();
- const [isEdit, setIsEdit] = useState(false);
- const [promptDetails, setPromptDetails] = useState(null);
- const [openSharePermissionModal, setOpenSharePermissionModal] =
- useState(false);
- const [isPermissionEdit, setIsPermissionEdit] = useState(false);
- const [isShareLoading, setIsShareLoading] = useState(false);
- const [allUserList, setAllUserList] = useState([]);
- const [loginModalOpen, setLoginModalOpen] = useState(true);
+ const getListApiCall = useCallback(({ axiosPrivate, sessionDetails }) => {
+ const baseUrl = getBaseUrl(sessionDetails);
- useEffect(() => {
- getListOfTools();
- }, []);
-
- useEffect(() => {
- setFilteredListOfTools(listOfTools);
- }, [listOfTools]);
-
- const getListOfTools = () => {
const requestOptions = {
method: "GET",
- url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/ `,
+ url: baseUrl,
headers: {
"X-CSRFToken": sessionDetails?.csrfToken,
},
};
+ return axiosPrivate(requestOptions);
+ }, []);
- setIsListLoading(true);
- axiosPrivate(requestOptions)
- .then((res) => {
- const data = res?.data;
- setListOfTools(data);
- setFilteredListOfTools(data);
- })
- .catch((err) => {
- setAlertDetails(
- handleException(err, "Failed to get the list of tools")
- );
- })
- .finally(() => {
- setIsListLoading(false);
- });
- };
+ const addItemApiCall = useCallback(
+ ({ axiosPrivate, sessionDetails, itemData }) => {
+ const baseUrl = getBaseUrl(sessionDetails);
- const handleAddNewTool = (body) => {
- let method = "POST";
- let url = `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/`;
- const isEdit = editItem && Object.keys(editItem)?.length > 0;
- if (isEdit) {
- method = "PATCH";
- url += `${editItem?.tool_id}/`;
- }
- return new Promise((resolve, reject) => {
const requestOptions = {
- method,
- url,
+ method: "POST",
+ url: baseUrl,
headers: {
"X-CSRFToken": sessionDetails?.csrfToken,
"Content-Type": "application/json",
},
- data: body,
+ data: itemData,
};
+ return axiosPrivate(requestOptions);
+ },
+ []
+ );
- axiosPrivate(requestOptions)
- .then((res) => {
- const tool = res?.data;
- updateList(isEdit, tool);
- setOpenAddTool(false);
- resolve(res?.data);
- })
- .catch((err) => {
- reject(err);
- });
- });
- };
-
- const updateList = (isEdit, data) => {
- let tools = [...listOfTools];
-
- if (isEdit) {
- tools = tools.map((item) =>
- item?.tool_id === data?.tool_id ? data : item
- );
- setEditItem(null);
- } else {
- tools.push(data);
- }
- setListOfTools(tools);
- };
-
- const handleEdit = (_event, tool) => {
- const editToolData = [...listOfTools].find(
- (item) => item?.tool_id === tool.tool_id
- );
- if (!editToolData) {
- return;
- }
- setIsEdit(true);
- setEditItem(editToolData);
- setOpenAddTool(true);
- };
-
- const handleDelete = (_event, tool) => {
- const requestOptions = {
- method: "DELETE",
- url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/${tool.tool_id}`,
- headers: {
- "X-CSRFToken": sessionDetails?.csrfToken,
- },
- };
-
- axiosPrivate(requestOptions)
- .then(() => {
- const tools = [...listOfTools].filter(
- (filterToll) => filterToll?.tool_id !== tool.tool_id
- );
- setListOfTools(tools);
- setAlertDetails({
- type: "success",
- content: `${tool?.tool_name} - Deleted successfully`,
- });
- })
- .catch((err) => {
- setAlertDetails(handleException(err, "Failed to Delete"));
- });
- };
-
- const onSearch = (search, setSearch) => {
- if (search?.length === 0) {
- setSearch(listOfTools);
- }
- const filteredList = [...listOfTools].filter((tool) => {
- const name = tool.tool_name?.toUpperCase();
- const searchUpperCase = search.toUpperCase();
- return name.includes(searchUpperCase);
- });
- setSearch(filteredList);
- };
-
- const showAddTool = () => {
- setEditItem(null);
- setIsEdit(false);
- setOpenAddTool(true);
- };
-
- const handleNewProjectBtnClick = () => {
- showAddTool();
-
- try {
- setPostHogCustomEvent("intent_new_ps_project", {
- info: "Clicked on '+ New Project' button",
- });
- } catch (err) {
- // If an error occurs while setting custom posthog event, ignore it and continue
- }
- };
-
- const CustomButtons = () => {
- return (
- }
- onClick={handleNewProjectBtnClick}
- >
- New Project
-
- );
- };
+ const editItemApiCall = useCallback(
+ ({ axiosPrivate, sessionDetails, itemData, itemId }) => {
+ const baseUrl = getBaseUrl(sessionDetails);
- const handleShare = (_event, promptProject, isEdit) => {
- const requestOptions = {
- method: "GET",
- url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/users/${promptProject?.tool_id}`,
- headers: {
- "X-CSRFToken": sessionDetails?.csrfToken,
- },
- };
- setIsShareLoading(true);
- getAllUsers();
- axiosPrivate(requestOptions)
- .then((res) => {
- setOpenSharePermissionModal(true);
- setPromptDetails(res?.data);
- setIsPermissionEdit(isEdit);
- })
- .catch((err) => {
- setAlertDetails(handleException(err));
- })
- .finally(() => {
- setIsShareLoading(false);
- });
- };
+ const requestOptions = {
+ method: "PATCH",
+ url: `${baseUrl}${itemId}/`,
+ headers: {
+ "X-CSRFToken": sessionDetails?.csrfToken,
+ "Content-Type": "application/json",
+ },
+ data: itemData,
+ };
+ return axiosPrivate(requestOptions);
+ },
+ []
+ );
- const getAllUsers = () => {
- setIsShareLoading(true);
- const requestOptions = {
- method: "GET",
- url: `/api/v1/unstract/${sessionDetails?.orgId}/users/`,
- };
+ const deleteItemApiCall = useCallback(
+ ({ axiosPrivate, sessionDetails, itemId }) => {
+ const baseUrl = getBaseUrl(sessionDetails);
- axiosPrivate(requestOptions)
- .then((response) => {
- const users = response?.data?.members || [];
- setAllUserList(
- users.map((user) => ({
- id: user?.id,
- email: user?.email,
- }))
- );
- })
- .catch((err) => {
- setAlertDetails(handleException(err, "Failed to load"));
- })
- .finally(() => {
- setIsShareLoading(false);
- });
- };
+ const requestOptions = {
+ method: "DELETE",
+ url: `${baseUrl}${itemId}`,
+ headers: {
+ "X-CSRFToken": sessionDetails?.csrfToken,
+ },
+ };
+ return axiosPrivate(requestOptions);
+ },
+ []
+ );
- const onShare = (userIds, adapter) => {
- const requestOptions = {
- method: "PATCH",
- url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/${adapter?.tool_id}`,
- headers: {
- "X-CSRFToken": sessionDetails?.csrfToken,
- },
- data: { shared_users: userIds },
- };
- axiosPrivate(requestOptions)
- .then((response) => {
- setOpenSharePermissionModal(false);
- })
- .catch((err) => {
- setAlertDetails(handleException(err, "Failed to load"));
- });
- };
+ const useListManagerHook = useListManager({
+ getListApiCall,
+ addItemApiCall,
+ editItemApiCall,
+ deleteItemApiCall,
+ searchProperty: "tool_name",
+ itemIdProp: "tool_id",
+ itemNameProp: "tool_name",
+ itemDescriptionProp: "description",
+ itemType: "Prompt Project",
+ });
+
+ const itemProps = useMemo(
+ () => ({
+ titleProp: "tool_name",
+ descriptionProp: "description",
+ iconProp: "icon",
+ idProp: "tool_id",
+ type: "Prompt Project",
+ }),
+ []
+ );
return (
- <>
-
-
- {openAddTool && (
-
- )}
-
- {!loginOnboardingMessage && OnboardMessagesModal && (
-
- )}
- >
+ }
+ itemProps={itemProps}
+ setPostHogCustomEvent={setPostHogCustomEvent}
+ newButtonEventName="intent_new_ps_project"
+ />
);
}
diff --git a/frontend/src/components/custom-tools/list-of-tools/ListOfToolsModal.jsx b/frontend/src/components/custom-tools/list-of-tools/ListOfToolsModal.jsx
new file mode 100644
index 000000000..1c0a3ca3b
--- /dev/null
+++ b/frontend/src/components/custom-tools/list-of-tools/ListOfToolsModal.jsx
@@ -0,0 +1,35 @@
+import PropTypes from "prop-types";
+import { useCallback } from "react";
+import { AddCustomToolFormModal } from "../add-custom-tool-form-modal/AddCustomToolFormModal";
+
+function ListOfToolsModal({ open, setOpen, editItem, isEdit, handleAddItem }) {
+ const handleAddNewTool = useCallback(
+ (itemData) => handleAddItem(itemData, editItem?.tool_id, isEdit),
+ [handleAddItem, isEdit, editItem?.tool_id]
+ );
+
+ return (
+
+ );
+}
+
+ListOfToolsModal.propTypes = {
+ open: PropTypes.bool.isRequired,
+ setOpen: PropTypes.func.isRequired,
+ editItem: PropTypes.shape({
+ tool_id: PropTypes.number,
+ tool_name: PropTypes.string,
+ description: PropTypes.string,
+ icon: PropTypes.string,
+ }),
+ isEdit: PropTypes.bool.isRequired,
+ handleAddItem: PropTypes.func.isRequired,
+};
+
+export default ListOfToolsModal;
diff --git a/frontend/src/components/custom-tools/view-tools/ViewTools.jsx b/frontend/src/components/custom-tools/view-tools/ViewTools.jsx
index 18ae88572..d185c1657 100644
--- a/frontend/src/components/custom-tools/view-tools/ViewTools.jsx
+++ b/frontend/src/components/custom-tools/view-tools/ViewTools.jsx
@@ -1,44 +1,48 @@
import PropTypes from "prop-types";
import { Typography } from "antd";
+import { useCallback } from "react";
import { ListView } from "../../widgets/list-view/ListView";
-import { SpinnerLoader } from "../../widgets/spinner-loader/SpinnerLoader.jsx";
+import { SpinnerLoader } from "../../widgets/spinner-loader/SpinnerLoader";
import "./ViewTools.css";
-import { EmptyState } from "../../widgets/empty-state/EmptyState.jsx";
+import { EmptyState } from "../../widgets/empty-state/EmptyState";
function ViewTools({
isLoading,
isEmpty,
- listOfTools,
- setOpenAddTool,
+ listOfTools = [],
+ setOpenAddTool = () => {},
handleEdit,
handleDelete,
titleProp,
- descriptionProp,
- iconProp,
+ descriptionProp = "",
+ iconProp = "",
idProp,
- centered,
+ centered = false,
isClickable = true,
- handleShare,
- showOwner,
- type,
+ handleShare = null,
+ showOwner = false,
+ type = "",
}) {
+ const handleEmptyStateClick = useCallback(() => {
+ setOpenAddTool(true);
+ }, [setOpenAddTool]);
+
if (isLoading) {
return ;
}
if (isEmpty) {
- let text = "No tools available";
- let btnText = "New Tool";
- if (type) {
- text = `No ${type.toLowerCase()} available`;
- btnText = type;
- }
+ const text = type
+ ? `No ${type.toLowerCase()} available`
+ : "No tools available";
+ const btnText = type || "New Tool";
+
return (
setOpenAddTool(true)}
+ handleClick={handleEmptyStateClick}
/>
);
}
diff --git a/frontend/src/components/navigations/tool-nav-bar/ToolNavBar.css b/frontend/src/components/navigations/tool-nav-bar/ToolNavBar.css
index cee99b0cd..1780a5116 100644
--- a/frontend/src/components/navigations/tool-nav-bar/ToolNavBar.css
+++ b/frontend/src/components/navigations/tool-nav-bar/ToolNavBar.css
@@ -29,3 +29,10 @@
.tool-bar-segment {
background-color: #0000000f;
}
+
+.tool-nav-title {
+ font-weight: 600;
+ font-size: 18px;
+ display: inline;
+ line-height: 24px;
+}
diff --git a/frontend/src/components/navigations/tool-nav-bar/ToolNavBar.jsx b/frontend/src/components/navigations/tool-nav-bar/ToolNavBar.jsx
index 79a995632..dde71bd24 100644
--- a/frontend/src/components/navigations/tool-nav-bar/ToolNavBar.jsx
+++ b/frontend/src/components/navigations/tool-nav-bar/ToolNavBar.jsx
@@ -5,6 +5,7 @@ import { ArrowLeftOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import { debounce } from "lodash";
import PropTypes from "prop-types";
+import { useCallback, useMemo, useEffect } from "react";
function ToolNavBar({
title,
@@ -17,9 +18,38 @@ function ToolNavBar({
onSearch,
}) {
const navigate = useNavigate();
- const onSearchDebounce = debounce(({ target: { value } }) => {
- onSearch(value, setSearchList);
- }, 600);
+
+ const handleBackClick = useCallback(() => {
+ if (previousRoute) {
+ navigate(previousRoute);
+ }
+ }, [previousRoute]);
+
+ const onSearchDebounce = useMemo(() => {
+ const debouncedFunction = debounce(({ target: { value } }) => {
+ if (onSearch) {
+ onSearch(value, setSearchList);
+ }
+ }, 600);
+ return debouncedFunction;
+ }, [onSearch, setSearchList]);
+
+ // Cleanup debounced function on unmount
+ useEffect(() => {
+ return () => {
+ onSearchDebounce.cancel();
+ };
+ }, [onSearchDebounce]);
+
+ // Handle segment change
+ const handleSegmentChange = useCallback(
+ (value) => {
+ if (segmentFilter) {
+ segmentFilter(value);
+ }
+ },
+ [segmentFilter]
+ );
return (
@@ -29,25 +59,14 @@ function ToolNavBar({
type="text"
shape="circle"
icon={}
- onClick={() => navigate(previousRoute)}
+ onClick={handleBackClick}
/>
)}
- {title && (
-
- {title}
-
- )}
+ {title && {title}}
{segmentFilter && segmentOptions && (
)}
@@ -70,7 +89,7 @@ function ToolNavBar({
ToolNavBar.propTypes = {
title: PropTypes.string,
enableSearch: PropTypes.bool,
- CustomButtons: PropTypes.func,
+ CustomButtons: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]),
setSearchList: PropTypes.func,
previousRoute: PropTypes.string,
segmentOptions: PropTypes.array,
diff --git a/frontend/src/components/tool-settings/list-of-items/ListOfItems.css b/frontend/src/components/tool-settings/list-of-items/ListOfItems.css
index fbde11618..22f02ea3e 100644
--- a/frontend/src/components/tool-settings/list-of-items/ListOfItems.css
+++ b/frontend/src/components/tool-settings/list-of-items/ListOfItems.css
@@ -1,14 +1,14 @@
/* Styles for ListOfItems */
.list-of-items-empty {
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
.cover-img .fit-cover {
- object-fit: cover;
- width: 100%;
- height: auto;
+ object-fit: cover;
+ width: 100%;
+ height: auto;
}
diff --git a/frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx b/frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx
index 3f633e4a0..d56512291 100644
--- a/frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx
+++ b/frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx
@@ -1,20 +1,16 @@
import { PlusOutlined } from "@ant-design/icons";
import PropTypes from "prop-types";
-import { useEffect, useState } from "react";
+import { useState, useCallback, useMemo } from "react";
-import { IslandLayout } from "../../../layouts/island-layout/IslandLayout";
-import { AddSourceModal } from "../../input-output/add-source-modal/AddSourceModal";
-import "../../input-output/data-source-card/DataSourceCard.css";
-import "./ToolSettings.css";
-import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate";
-import { useAlertStore } from "../../../store/alert-store";
import { useSessionStore } from "../../../store/session-store";
-import { CustomButton } from "../../widgets/custom-button/CustomButton";
-import { useExceptionHandler } from "../../../hooks/useExceptionHandler";
-import { ToolNavBar } from "../../navigations/tool-nav-bar/ToolNavBar";
-import { ViewTools } from "../../custom-tools/view-tools/ViewTools";
-import { SharePermission } from "../../widgets/share-permission/SharePermission";
import usePostHogEvents from "../../../hooks/usePostHogEvents";
+import { useListManager } from "../../../hooks/useListManager";
+import { ListView } from "../../view-projects/ListView";
+import ToolSettingsModal from "./ToolSettingsModal";
+import { SharePermission } from "../../widgets/share-permission/SharePermission";
+import { useAlertStore } from "../../../store/alert-store";
+import { useExceptionHandler } from "../../../hooks/useExceptionHandler";
+import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate";
const titles = {
llm: "LLMs",
@@ -33,123 +29,96 @@ const btnText = {
};
function ToolSettings({ type }) {
- const [tableRows, setTableRows] = useState([]);
- const [isLoading, setIsLoading] = useState(false);
- const [isShareLoading, setIsShareLoading] = useState(false);
+ const { setPostHogCustomEvent, posthogEventText } = usePostHogEvents();
+ const { setAlertDetails } = useAlertStore();
+ const handleException = useExceptionHandler();
+ const axiosPrivate = useAxiosPrivate();
+ const { sessionDetails } = useSessionStore();
+
const [adapterDetails, setAdapterDetails] = useState(null);
const [userList, setUserList] = useState([]);
- const [openAddSourcesModal, setOpenAddSourcesModal] = useState(false);
+ const [isShareLoading, setIsShareLoading] = useState(false);
const [openSharePermissionModal, setOpenSharePermissionModal] =
useState(false);
- const [isPermissonEdit, setIsPermissionEdit] = useState(false);
- const [editItemId, setEditItemId] = useState(null);
- const { sessionDetails } = useSessionStore();
- const { setAlertDetails } = useAlertStore();
- const axiosPrivate = useAxiosPrivate();
- const handleException = useExceptionHandler();
- const { posthogEventText, setPostHogCustomEvent } = usePostHogEvents();
-
- useEffect(() => {
- setTableRows([]);
- if (!type) {
- return;
- }
- getAdapters();
- }, [type]);
-
- const getAdapters = () => {
- const requestOptions = {
- method: "GET",
- url: `/api/v1/unstract/${
- sessionDetails?.orgId
- }/adapter?adapter_type=${type.toUpperCase()}`,
- };
- setIsLoading(true);
- axiosPrivate(requestOptions)
- .then((res) => {
- setTableRows(res?.data);
- })
- .catch((err) => {
- setAlertDetails(handleException(err));
- })
- .finally(() => {
- setIsLoading(false);
- });
- };
-
- const addNewItem = (row, isEdit) => {
- if (isEdit) {
- const rowsModified = [...tableRows].map((tableRow) => {
- if (tableRow?.id !== row?.id) {
- return tableRow;
- }
- tableRow["adapter_name"] = row?.adapter_name;
- return tableRow;
- });
- setTableRows(rowsModified);
- } else {
- const rowsModified = [...tableRows];
- rowsModified.push(row);
- setTableRows(rowsModified);
- }
- };
+ const [isPermissionEdit, setIsPermissionEdit] = useState(false);
+
+ const adapterBaseUrl = `/api/v1/unstract/${sessionDetails?.orgId}/adapter`;
+ const userBaseUrl = `/api/v1/unstract/${sessionDetails?.orgId}/users`;
+
+ const getListApiCall = useCallback(
+ ({ axiosPrivate, sessionDetails }) => {
+ const requestOptions = {
+ method: "GET",
+ url: `${adapterBaseUrl}?adapter_type=${type.toUpperCase()}`,
+ };
+ return axiosPrivate(requestOptions);
+ },
+ [type, adapterBaseUrl]
+ );
- const handleDelete = (_event, adapter) => {
- const requestOptions = {
- method: "DELETE",
- url: `/api/v1/unstract/${sessionDetails?.orgId}/adapter/${adapter?.id}/`,
- headers: {
- "X-CSRFToken": sessionDetails?.csrfToken,
- },
- };
+ const addItemApiCall = useCallback(
+ ({ axiosPrivate, sessionDetails, itemData }) => {
+ const requestOptions = {
+ method: "POST",
+ url: `${adapterBaseUrl}/`,
+ headers: {
+ "X-CSRFToken": sessionDetails?.csrfToken,
+ "Content-Type": "application/json",
+ },
+ data: itemData,
+ };
+ return axiosPrivate(requestOptions);
+ },
+ [adapterBaseUrl]
+ );
- setIsLoading(true);
- axiosPrivate(requestOptions)
- .then((res) => {
- const filteredList = tableRows.filter((row) => row?.id !== adapter?.id);
- setTableRows(filteredList);
- setAlertDetails({
- type: "success",
- content: "Successfully deleted",
- });
- })
- .catch((err) => {
- setAlertDetails(handleException(err));
- })
- .finally(() => {
- setIsLoading(false);
- });
- };
+ const editItemApiCall = useCallback(
+ ({ axiosPrivate, sessionDetails, itemData, itemId }) => {
+ const requestOptions = {
+ method: "PATCH",
+ url: `${adapterBaseUrl}/${itemId}/`,
+ headers: {
+ "X-CSRFToken": sessionDetails?.csrfToken,
+ "Content-Type": "application/json",
+ },
+ data: itemData,
+ };
+ return axiosPrivate(requestOptions);
+ },
+ [adapterBaseUrl]
+ );
- const handleShare = (_event, adapter, isEdit) => {
- const requestOptions = {
- method: "GET",
- url: `/api/v1/unstract/${sessionDetails?.orgId}/adapter/users/${adapter.id}/`,
- headers: {
- "X-CSRFToken": sessionDetails?.csrfToken,
- },
- };
- setIsShareLoading(true);
- getAllUsers();
- axiosPrivate(requestOptions)
- .then((res) => {
- setOpenSharePermissionModal(true);
- setAdapterDetails(res?.data);
- setIsPermissionEdit(isEdit);
- })
- .catch((err) => {
- setAlertDetails(handleException(err));
- })
- .finally(() => {
- setIsShareLoading(false);
- });
- };
+ const deleteItemApiCall = useCallback(
+ ({ axiosPrivate, sessionDetails, itemId }) => {
+ const requestOptions = {
+ method: "DELETE",
+ url: `${adapterBaseUrl}/${itemId}/`,
+ headers: {
+ "X-CSRFToken": sessionDetails?.csrfToken,
+ },
+ };
+ return axiosPrivate(requestOptions);
+ },
+ [adapterBaseUrl]
+ );
- const getAllUsers = () => {
+ const useListManagerHook = useListManager({
+ getListApiCall,
+ addItemApiCall,
+ editItemApiCall,
+ deleteItemApiCall,
+ searchProperty: "adapter_name",
+ itemIdProp: "id",
+ itemNameProp: "adapter_name",
+ itemDescriptionProp: "description",
+ itemType: "Adapter",
+ });
+
+ const getAllUsers = useCallback(() => {
setIsShareLoading(true);
const requestOptions = {
method: "GET",
- url: `/api/v1/unstract/${sessionDetails?.orgId}/users/`,
+ url: `${userBaseUrl}/`,
};
axiosPrivate(requestOptions)
@@ -157,8 +126,8 @@ function ToolSettings({ type }) {
const users = response?.data?.members || [];
setUserList(
users.map((user) => ({
- id: user?.id,
- email: user?.email,
+ id: user.id,
+ email: user.email,
}))
);
})
@@ -168,95 +137,100 @@ function ToolSettings({ type }) {
.finally(() => {
setIsShareLoading(false);
});
- };
-
- const onShare = (userIds, adapter) => {
- const requestOptions = {
- method: "PATCH",
- url: `/api/v1/unstract/${sessionDetails?.orgId}/adapter/${adapter?.id}/`,
- headers: {
- "X-CSRFToken": sessionDetails?.csrfToken,
- },
- data: { shared_users: userIds },
- };
- axiosPrivate(requestOptions)
- .then((response) => {
- setOpenSharePermissionModal(false);
- })
- .catch((err) => {
- setAlertDetails(handleException(err, "Failed to load"));
- });
- };
+ }, [userBaseUrl]);
+
+ const handleShare = useCallback(
+ (adapter, isEdit) => {
+ const requestOptions = {
+ method: "GET",
+ url: `${adapterBaseUrl}/users/${adapter.id}/`,
+ headers: {
+ "X-CSRFToken": sessionDetails?.csrfToken,
+ },
+ };
+ setIsShareLoading(true);
+ getAllUsers();
+ axiosPrivate(requestOptions)
+ .then((res) => {
+ setOpenSharePermissionModal(true);
+ setAdapterDetails(res.data);
+ setIsPermissionEdit(isEdit);
+ })
+ .catch((err) => {
+ setAlertDetails(handleException(err));
+ })
+ .finally(() => {
+ setIsShareLoading(false);
+ });
+ },
+ [
+ getAllUsers,
+ setOpenSharePermissionModal,
+ setAdapterDetails,
+ setIsPermissionEdit,
+ adapterBaseUrl,
+ ]
+ );
- const handleOpenAddSourceModal = () => {
- setOpenAddSourcesModal(true);
+ const onShare = useCallback(
+ (userIds, adapter) => {
+ const requestOptions = {
+ method: "PATCH",
+ url: `${adapterBaseUrl}/${adapter.id}/`,
+ headers: {
+ "X-CSRFToken": sessionDetails?.csrfToken,
+ },
+ data: { shared_users: userIds },
+ };
+ axiosPrivate(requestOptions)
+ .then(() => {
+ setOpenSharePermissionModal(false);
+ })
+ .catch((err) => {
+ setAlertDetails(handleException(err, "Failed to load"));
+ });
+ },
+ [setOpenSharePermissionModal, adapterBaseUrl]
+ );
- try {
- setPostHogCustomEvent(posthogEventText[type], {
- info: `Clicked on '+ ${btnText[type]}' button`,
- });
- } catch (err) {
- // If an error occurs while setting custom posthog event, ignore it and continue
- }
- };
+ const itemProps = useMemo(
+ () => ({
+ titleProp: "adapter_name",
+ descriptionProp: "description",
+ iconProp: "icon",
+ idProp: "id",
+ type: "Adapter",
+ handleShare,
+ showOwner: true,
+ isClickable: false,
+ centered: true,
+ }),
+ [handleShare]
+ );
return (
-
-
+ {
- return (
- }
- >
- {btnText[type]}
-
- );
- }}
- />
-
-
-
- setEditItemId(item?.id)}
- idProp="id"
- titleProp="adapter_name"
- descriptionProp="description"
- iconProp="icon"
- isEmpty={!tableRows?.length}
- centered
- isClickable={false}
- handleShare={handleShare}
- showOwner={true}
- type="Adapter"
- />
-
-
-
- }
+ itemProps={itemProps}
+ setPostHogCustomEvent={setPostHogCustomEvent}
+ newButtonEventName={posthogEventText[type]}
type={type}
- addNewItem={addNewItem}
- editItemId={editItemId}
- setEditItemId={setEditItemId}
/>
-
+ >
);
}
diff --git a/frontend/src/components/tool-settings/tool-settings/ToolSettingsModal.jsx b/frontend/src/components/tool-settings/tool-settings/ToolSettingsModal.jsx
new file mode 100644
index 000000000..49e6868a1
--- /dev/null
+++ b/frontend/src/components/tool-settings/tool-settings/ToolSettingsModal.jsx
@@ -0,0 +1,42 @@
+import PropTypes from "prop-types";
+import { useCallback } from "react";
+import { AddSourceModal } from "../../input-output/add-source-modal/AddSourceModal";
+
+function ToolSettingsModal({
+ open,
+ setOpen,
+ editItem,
+ isEdit,
+ type,
+ updateList,
+}) {
+ const handleAddNewItem = useCallback(
+ (itemData) => {
+ updateList(itemData, editItem?.id, isEdit);
+ setOpen(false);
+ },
+ [isEdit, editItem?.id]
+ );
+
+ return (
+ {}}
+ />
+ );
+}
+
+ToolSettingsModal.propTypes = {
+ open: PropTypes.bool.isRequired,
+ setOpen: PropTypes.func.isRequired,
+ editItem: PropTypes.object,
+ isEdit: PropTypes.bool.isRequired,
+ type: PropTypes.string.isRequired,
+ updateList: PropTypes.func.isRequired,
+};
+
+export default ToolSettingsModal;
diff --git a/frontend/src/components/view-projects/ListView.jsx b/frontend/src/components/view-projects/ListView.jsx
new file mode 100644
index 000000000..e3f703d08
--- /dev/null
+++ b/frontend/src/components/view-projects/ListView.jsx
@@ -0,0 +1,159 @@
+import { useEffect, useState, useCallback } from "react";
+import PropTypes from "prop-types";
+import { CustomButton } from "../widgets/custom-button/CustomButton";
+import { ToolNavBar } from "../navigations/tool-nav-bar/ToolNavBar";
+import { ViewTools } from "../custom-tools/view-tools/ViewTools";
+import "./ViewProjects.css";
+
+function ListView({
+ title,
+ useListManagerHook,
+ CustomModalComponent,
+ customButtonText,
+ customButtonIcon,
+ onCustomButtonClick,
+ handleEditItem,
+ handleDeleteItem,
+ itemProps,
+ setPostHogCustomEvent,
+ newButtonEventName,
+ type,
+}) {
+ const {
+ list,
+ filteredList,
+ loading,
+ fetchList,
+ handleSearch,
+ handleAddItem,
+ handleDeleteItem: deleteItem,
+ updateList,
+ } = useListManagerHook;
+
+ const [openModal, setOpenModal] = useState(false);
+ const [editingItem, setEditingItem] = useState(null);
+ const [isEdit, setIsEdit] = useState(false);
+
+ useEffect(() => {
+ fetchList();
+ }, [title]);
+
+ const handleCustomButtonClick = useCallback(() => {
+ setEditingItem(null);
+ setIsEdit(false);
+ setOpenModal(true);
+ if (onCustomButtonClick) onCustomButtonClick();
+ try {
+ setPostHogCustomEvent?.(newButtonEventName, {
+ info: `Clicked on '${customButtonText}' button`,
+ });
+ } catch (err) {
+ // Ignore error
+ }
+ }, [onCustomButtonClick, newButtonEventName, customButtonText]);
+
+ const CustomButtons = useCallback(
+ () => (
+
+ {customButtonText}
+
+ ),
+ [customButtonIcon, handleCustomButtonClick, customButtonText]
+ );
+
+ const handleEdit = useCallback(
+ (_event, item) => {
+ setEditingItem(item);
+ setIsEdit(true);
+ setOpenModal(true);
+ if (handleEditItem) handleEditItem(item);
+ },
+ [handleEditItem]
+ );
+
+ const handleDelete = useCallback(
+ (_event, item) => {
+ deleteItem(item?.[itemProps?.idProp]);
+ if (handleDeleteItem) handleDeleteItem(item);
+ },
+ [deleteItem, handleDeleteItem, itemProps?.idProp]
+ );
+
+ return (
+ <>
+
+
+ {openModal && CustomModalComponent && (
+
+ )}
+ >
+ );
+}
+
+ListView.propTypes = {
+ title: PropTypes.string.isRequired,
+ useListManagerHook: PropTypes.shape({
+ list: PropTypes.array.isRequired,
+ filteredList: PropTypes.array.isRequired,
+ loading: PropTypes.bool.isRequired,
+ fetchList: PropTypes.func.isRequired,
+ handleSearch: PropTypes.func.isRequired,
+ handleAddItem: PropTypes.func.isRequired,
+ handleDeleteItem: PropTypes.func.isRequired,
+ updateList: PropTypes.func.isRequired,
+ }).isRequired,
+ CustomModalComponent: PropTypes.oneOfType([
+ PropTypes.func,
+ PropTypes.elementType,
+ ]),
+ customButtonText: PropTypes.string.isRequired,
+ customButtonIcon: PropTypes.element,
+ onCustomButtonClick: PropTypes.func,
+ handleEditItem: PropTypes.func,
+ handleDeleteItem: PropTypes.func,
+ itemProps: PropTypes.shape({
+ titleProp: PropTypes.string.isRequired,
+ descriptionProp: PropTypes.string,
+ iconProp: PropTypes.string,
+ idProp: PropTypes.string.isRequired,
+ type: PropTypes.string.isRequired,
+ }).isRequired,
+ setPostHogCustomEvent: PropTypes.func,
+ newButtonEventName: PropTypes.string,
+ type: PropTypes.string,
+};
+
+export { ListView };
diff --git a/frontend/src/components/view-projects/ViewProjects.css b/frontend/src/components/view-projects/ViewProjects.css
new file mode 100644
index 000000000..1407e7692
--- /dev/null
+++ b/frontend/src/components/view-projects/ViewProjects.css
@@ -0,0 +1,52 @@
+/* Styles for ListOfTools */
+
+.list-of-tools-layout {
+ height: 100%;
+ background-color: var(--page-bg-2);
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ overflow-y: hidden;
+}
+
+.list-of-tools-island {
+ background-color: var(--page-bg-1);
+ flex: 1;
+ padding: 20px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.list-of-tools-wrap {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.list-of-tools-header {
+ display: grid;
+ grid-template-columns: auto 1fr;
+}
+
+.list-of-tools-title {
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.list-of-tools-header2 {
+ display: grid;
+ grid-auto-flow: column;
+ column-gap: 10px;
+ justify-self: end;
+}
+
+.list-of-tools-divider {
+ margin-top: 12px;
+}
+
+.list-of-tools-body {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+}
diff --git a/frontend/src/components/widgets/list-view/ListView.css b/frontend/src/components/widgets/list-view/ListView.css
index 5d586fec9..2a9113a57 100644
--- a/frontend/src/components/widgets/list-view/ListView.css
+++ b/frontend/src/components/widgets/list-view/ListView.css
@@ -52,17 +52,11 @@
}
.adapter-cover-img .fit-cover {
- width: 120px;
- height: 90px;
+ width: 50px;
+ height: 50px;
object-fit: contain;
}
-.adapters-list-profile-container {
- align-items: center;
- display: flex;
- justify-content: center;
-}
-
.adapters-list-user-prefix {
margin: 0 5px;
font-weight: 500;
diff --git a/frontend/src/components/widgets/list-view/ListView.jsx b/frontend/src/components/widgets/list-view/ListView.jsx
index e34011539..ff5d4aa09 100644
--- a/frontend/src/components/widgets/list-view/ListView.jsx
+++ b/frontend/src/components/widgets/list-view/ListView.jsx
@@ -1,9 +1,9 @@
import {
Avatar,
- Flex,
Image,
- List,
Popconfirm,
+ Space,
+ Table,
Tooltip,
Typography,
} from "antd";
@@ -17,6 +17,8 @@ import {
UserOutlined,
} from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
+import moment from "moment";
+import { useMemo, useCallback } from "react";
import { useSessionStore } from "../../../store/session-store";
@@ -24,157 +26,236 @@ function ListView({
listOfTools,
handleEdit,
handleDelete,
- handleShare,
+ handleShare = null,
titleProp,
- descriptionProp,
- iconProp,
+ descriptionProp = "",
+ iconProp = "",
idProp,
- centered,
+ centered = false,
isClickable = true,
- showOwner = true,
- type,
+ type = "",
}) {
const navigate = useNavigate();
const { sessionDetails } = useSessionStore();
- const handleDeleteClick = (event, tool) => {
- event.stopPropagation(); // Stop propagation to prevent list item click
- handleDelete(event, tool);
- };
- const handleShareClick = (event, tool, isEdit) => {
- event.stopPropagation(); // Stop propagation to prevent list item click
- handleShare(event, tool, isEdit);
- };
+ const handleRowClick = useCallback(
+ (record) => {
+ if (isClickable) {
+ navigate(`${record[idProp]}`);
+ }
+ },
+ [navigate, idProp, isClickable]
+ );
- const renderTitle = (item) => {
- let title = null;
- if (iconProp && item[iconProp].length > 4) {
- title = (
-
-
+ const renderNameColumn = useCallback(
+ (text, record) => {
+ let titleContent = null;
+ if (iconProp && record[iconProp]?.length > 4) {
+ titleContent = (
+
+
+
+ {record[titleProp]}
+
+
+ );
+ } else if (iconProp) {
+ titleContent = (
- {item[titleProp]}
+ {`${record[iconProp]} ${record[titleProp]}`}
-
- );
- } else if (iconProp) {
- title = (
-
- {`${item[iconProp]} ${item[titleProp]}`}
-
+ );
+ } else {
+ titleContent = (
+
+ {record[titleProp]}
+
+ );
+ }
+ return (
+ <>
+
+ {titleContent}
+
+
+
+ {record?.[descriptionProp]}
+
+
+ >
);
- } else {
- title = (
-
- {item[titleProp]}
-
+ },
+ [iconProp, titleProp, descriptionProp]
+ );
+
+ const renderOwnedByColumn = useCallback(
+ (text) => {
+ const owner = text === sessionDetails?.email ? "Me" : text || "-";
+ return (
+
+
}
+ />
+
+ Owned By:
+
+
{owner}
+
);
- }
+ },
+ [sessionDetails?.email]
+ );
+
+ const renderDateColumn = useCallback(
+ (text) => (text ? moment(text).format("YYYY-MM-DD HH:mm:ss") : "-"),
+ []
+ );
+
+ const handleEditClick = useCallback(
+ (event, record) => {
+ event.stopPropagation();
+ handleEdit(event, record);
+ },
+ [handleEdit]
+ );
+
+ const handleShareClick = useCallback(
+ (event, record) => {
+ event.stopPropagation();
+ handleShare?.(record, true);
+ },
+ [handleShare]
+ );
- return (
- {
+ event.stopPropagation();
+ handleDelete(event, record);
+ },
+ [handleDelete]
+ );
+
+ const renderActionsColumn = useCallback(
+ (text, record) => (
+ event.stopPropagation()}
+ role="none"
>
-
-
- {title}
-
- {showOwner && (
-
-
}
- />
-
- Owned By:
-
-
- {item?.created_by_email
- ? item?.created_by_email === sessionDetails.email
- ? "Me"
- : item?.created_by_email
- : "-"}
-
-
- )}
-
-
event.stopPropagation()}
- role="none"
- >
-
handleEdit(event, item)}
- className="action-icon-buttons edit-icon"
+ handleEditClick(event, record)}
+ className="action-icon-buttons edit-icon"
+ />
+ {handleShare && (
+ handleShareClick(event, record)}
/>
- {handleShare && (
- handleShareClick(event, item, true)}
- />
- )}
- }
- onConfirm={(event) => {
- handleDeleteClick(event, item);
- }}
- >
-
-
-
-
-
-
- );
- };
+ )}
+
}
+ onConfirm={(event) => {
+ handleDeleteClick(event, record);
+ }}
+ >
+
+
+
+
+
+ ),
+ [
+ idProp,
+ titleProp,
+ type,
+ handleEditClick,
+ handleShare,
+ handleShareClick,
+ handleDeleteClick,
+ ]
+ );
+
+ const columns = useMemo(
+ () => [
+ {
+ title: "Name",
+ dataIndex: titleProp,
+ key: "name",
+ sorter: (a, b) =>
+ a[titleProp]
+ ?.toLowerCase()
+ .localeCompare(b[titleProp]?.toLowerCase()),
+ render: renderNameColumn,
+ },
+ {
+ title: "Owned By",
+ dataIndex: "created_by_email",
+ key: "ownedBy",
+ sorter: (a, b) =>
+ (a.created_by_email || "")
+ .toLowerCase()
+ .localeCompare((b.created_by_email || "").toLowerCase()),
+ render: renderOwnedByColumn,
+ },
+ {
+ title: "Created At",
+ dataIndex: "created_at",
+ key: "createdAt",
+ sorter: (a, b) =>
+ new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
+ render: renderDateColumn,
+ },
+ {
+ title: "Modified At",
+ dataIndex: "modified_at",
+ key: "modifiedAt",
+ sorter: (a, b) =>
+ new Date(a.modified_at).getTime() - new Date(b.modified_at).getTime(),
+ render: renderDateColumn,
+ },
+ {
+ title: "Actions",
+ key: "actions",
+ align: "center",
+ render: renderActionsColumn,
+ },
+ ],
+ [
+ titleProp,
+ renderNameColumn,
+ renderOwnedByColumn,
+ renderDateColumn,
+ renderActionsColumn,
+ ]
+ );
return (
- {
- return (
- {
- isClickable
- ? navigate(`${item[idProp]}`)
- : handleShareClick(event, item, false);
- }}
- className={`cur-pointer ${centered ? "centered" : ""}`}
- >
-
-
- {item[descriptionProp]}
-
-
- }
- >
-
- );
- }}
+ onRow={(record) => ({
+ onClick: () => handleRowClick(record),
+ className: `cur-pointer ${centered ? "centered" : ""}`,
+ })}
+ className="width-100"
/>
);
}
@@ -190,7 +271,6 @@ ListView.propTypes = {
idProp: PropTypes.string.isRequired,
centered: PropTypes.bool,
isClickable: PropTypes.bool,
- showOwner: PropTypes.bool,
type: PropTypes.string,
};
diff --git a/frontend/src/components/workflows/new-workflow/NewWorkflow.jsx b/frontend/src/components/workflows/new-workflow/NewWorkflow.jsx
index d6f0420b0..8024fc933 100644
--- a/frontend/src/components/workflows/new-workflow/NewWorkflow.jsx
+++ b/frontend/src/components/workflows/new-workflow/NewWorkflow.jsx
@@ -9,7 +9,7 @@ function NewWorkflow({
description = "",
onDone = () => {},
onClose = () => {},
- loading = {},
+ loading,
toggleModal = () => {},
openModal = {},
backendErrors,
diff --git a/frontend/src/components/workflows/workflow/WorkflowModal.jsx b/frontend/src/components/workflows/workflow/WorkflowModal.jsx
new file mode 100644
index 000000000..543ecaf9b
--- /dev/null
+++ b/frontend/src/components/workflows/workflow/WorkflowModal.jsx
@@ -0,0 +1,79 @@
+import PropTypes from "prop-types";
+import { useState } from "react";
+import { LazyLoader } from "../../widgets/lazy-loader/LazyLoader.jsx";
+import { useAlertStore } from "../../../store/alert-store";
+import { useExceptionHandler } from "../../../hooks/useExceptionHandler.jsx";
+import { useWorkflowStore } from "../../../store/workflow-store";
+import { useNavigate } from "react-router-dom";
+import { useSessionStore } from "../../../store/session-store";
+
+function WorkflowModal({
+ open,
+ setOpen,
+ editItem,
+ isEdit,
+ handleAddItem,
+ loading,
+ backendErrors,
+}) {
+ const { setAlertDetails } = useAlertStore();
+ const handleException = useExceptionHandler();
+ const [localBackendErrors, setLocalBackendErrors] = useState(backendErrors);
+ const navigate = useNavigate();
+ const { updateWorkflow } = useWorkflowStore();
+ const sessionDetails = useSessionStore((state) => state?.sessionDetails);
+ const orgName = sessionDetails?.orgName ?? "";
+
+ const handleOnDone = (name, description) => {
+ handleAddItem({ name, description }, editItem?.id, isEdit)
+ .then((project) => {
+ if (!isEdit) {
+ updateWorkflow({ projectName: project?.workflow_name ?? "" });
+ navigate(`/${orgName}/workflows/${project?.id ?? ""}`);
+ }
+ setAlertDetails({
+ type: "success",
+ content: "Workflow updated successfully",
+ });
+ setOpen(false);
+ })
+ .catch((err) => {
+ handleException(err, "", setLocalBackendErrors);
+ });
+ };
+
+ const handleOnClose = () => {
+ setOpen(false);
+ };
+
+ return (
+ import("../new-workflow/NewWorkflow.jsx")}
+ componentName="NewWorkflow"
+ name={editItem?.workflow_name ?? ""}
+ description={editItem?.description ?? ""}
+ onDone={handleOnDone}
+ onClose={handleOnClose}
+ loading={loading}
+ openModal={open}
+ backendErrors={localBackendErrors}
+ setBackendErrors={setLocalBackendErrors}
+ />
+ );
+}
+
+WorkflowModal.propTypes = {
+ open: PropTypes.bool.isRequired,
+ setOpen: PropTypes.func.isRequired,
+ editItem: PropTypes.shape({
+ id: PropTypes.number,
+ workflow_name: PropTypes.string,
+ description: PropTypes.string,
+ }),
+ isEdit: PropTypes.bool.isRequired,
+ handleAddItem: PropTypes.func.isRequired,
+ loading: PropTypes.bool.isRequired,
+ backendErrors: PropTypes.any,
+};
+
+export default WorkflowModal;
diff --git a/frontend/src/components/workflows/workflow/Workflows.jsx b/frontend/src/components/workflows/workflow/Workflows.jsx
index 08839404f..4c327fd3d 100644
--- a/frontend/src/components/workflows/workflow/Workflows.jsx
+++ b/frontend/src/components/workflows/workflow/Workflows.jsx
@@ -1,273 +1,63 @@
-import { PlusOutlined, UserOutlined } from "@ant-design/icons";
-import { Typography } from "antd";
-import isEmpty from "lodash/isEmpty";
-import PropTypes from "prop-types";
-import { useEffect, useRef, useState } from "react";
-import { useLocation, useNavigate } from "react-router-dom";
-
-import { useAlertStore } from "../../../store/alert-store";
-import { useSessionStore } from "../../../store/session-store";
-import { useWorkflowStore } from "../../../store/workflow-store";
-import { CustomButton } from "../../widgets/custom-button/CustomButton.jsx";
-import { EmptyState } from "../../widgets/empty-state/EmptyState.jsx";
-import { LazyLoader } from "../../widgets/lazy-loader/LazyLoader.jsx";
-import { SpinnerLoader } from "../../widgets/spinner-loader/SpinnerLoader.jsx";
-import "./Workflows.css";
+import { PlusOutlined } from "@ant-design/icons";
+import usePostHogEvents from "../../../hooks/usePostHogEvents";
+import { useListManager } from "../../../hooks/useListManager";
+import WorkflowModal from "./WorkflowModal";
+import { ListView } from "../../view-projects/ListView";
import { workflowService } from "./workflow-service";
-import { useExceptionHandler } from "../../../hooks/useExceptionHandler.jsx";
-import { ToolNavBar } from "../../navigations/tool-nav-bar/ToolNavBar.jsx";
-import { ViewTools } from "../../custom-tools/view-tools/ViewTools.jsx";
-import usePostHogEvents from "../../../hooks/usePostHogEvents.js";
-
-const PROJECT_FILTER_OPTIONS = [
- { label: "My Workflows", value: "mine" },
- { label: "Organization Workflows", value: "all" },
-];
-
-const { Title, Text } = Typography;
function Workflows() {
- const navigate = useNavigate();
- const location = useLocation();
- const projectApiService = workflowService();
- const handleException = useExceptionHandler();
const { setPostHogCustomEvent } = usePostHogEvents();
+ const projectApiService = workflowService();
- const [projectList, setProjectList] = useState();
- const [editingProject, setEditProject] = useState();
- const [loading, setLoading] = useState(false);
- const [openModal, toggleModal] = useState(true);
- const projectListRef = useRef();
- const filterViewRef = useRef(PROJECT_FILTER_OPTIONS[0].value);
- const [backendErrors, setBackendErrors] = useState(null);
-
- const { setAlertDetails } = useAlertStore();
- const sessionDetails = useSessionStore((state) => state?.sessionDetails);
- const { updateWorkflow } = useWorkflowStore();
- const orgName = sessionDetails?.orgName;
-
- useEffect(() => {
- if (location.pathname === `/${orgName}/workflows`) {
- getProjectList();
- }
- }, [location.pathname]);
-
- function getProjectList() {
- projectApiService
- .getProjectList(filterViewRef.current === PROJECT_FILTER_OPTIONS[0].value)
- .then((res) => {
- projectListRef.current = res.data;
- setProjectList(res.data);
- })
- .catch(() => {
- console.error("Unable to get project list");
- });
- }
+ const getListApiCall = ({ initialFilter }) =>
+ projectApiService.getProjectList(initialFilter);
- function onSearch(searchText, setSearchList) {
- if (!searchText.trim()) {
- setSearchList(projectListRef.current);
- return;
- }
- const filteredList = projectListRef.current.filter((item) =>
- item.workflow_name.toLowerCase().includes(searchText.toLowerCase())
+ const addItemApiCall = ({ itemData }) =>
+ projectApiService.editProject(
+ itemData?.name ?? "",
+ itemData?.description ?? ""
);
- setSearchList(filteredList);
- }
-
- function applyFilter(value) {
- filterViewRef.current = value;
- projectListRef.current = "";
- setProjectList("");
- getProjectList();
- }
-
- function editProject(name, description) {
- setLoading(true);
- projectApiService
- .editProject(name, description, editingProject?.id)
- .then((res) => {
- closeNewProject();
- if (editingProject?.name) {
- getProjectList();
- } else {
- openProject(res.data);
- }
- setAlertDetails({
- type: "success",
- content: "Workflow updated successfully",
- });
- getProjectList();
- })
- .catch((err) => {
- handleException(err, "", setBackendErrors);
- })
- .finally(() => {
- setLoading(false);
- });
- }
-
- function openProject(project) {
- updateWorkflow({ projectName: project?.workflow_name });
- navigate(`/${orgName}/workflows/${project.id}`);
- }
- function showNewProject() {
- setEditProject({ name: "", description: "" });
- }
-
- function updateProject(_event, project) {
- toggleModal(true);
- setEditProject({
- name: project.workflow_name,
- description: project.description || "",
- id: project.id,
- });
- }
-
- const canDeleteProject = async (id) => {
- let status = false;
- await projectApiService.canUpdate(id).then((res) => {
- status = res?.data?.can_update || false;
- });
- return status;
- };
-
- const deleteProject = async (_evt, project) => {
- const canDelete = await canDeleteProject(project.id);
- if (canDelete) {
- projectApiService
- .deleteProject(project.id)
- .then(() => {
- getProjectList();
- setAlertDetails({
- type: "success",
- content: "Workflow deleted successfully",
- });
- })
- .catch(() => {
- setAlertDetails({
- type: "error",
- content: `Unable to delete workflow ${project.id}`,
- });
- });
- } else {
- setAlertDetails({
- type: "error",
- content:
- "Cannot delete this Workflow, since it is used in one or many of the API/ETL/Task pipelines",
- });
- }
- };
-
- function closeNewProject() {
- setEditProject();
- }
-
- const handleNewWorkflowBtnClick = () => {
- showNewProject();
- toggleModal(true);
-
- try {
- setPostHogCustomEvent("intent_new_wf_project", {
- info: "Clicked on '+ New Workflow' button",
- });
- } catch (err) {
- // If an error occurs while setting custom posthog event, ignore it and continue
- }
- };
-
- const CustomButtons = () => {
- return (
- }
- onClick={handleNewWorkflowBtnClick}
- >
- New Workflow
-
+ const editItemApiCall = ({ itemData, itemId }) =>
+ projectApiService.editProject(
+ itemData?.name ?? "",
+ itemData?.description ?? "",
+ itemId
);
- };
+
+ const deleteItemApiCall = ({ itemId }) =>
+ projectApiService.deleteProject(itemId);
+
+ const useListManagerHook = useListManager({
+ getListApiCall,
+ addItemApiCall,
+ editItemApiCall,
+ deleteItemApiCall,
+ searchProperty: "workflow_name",
+ itemIdProp: "id",
+ itemNameProp: "workflow_name",
+ itemDescriptionProp: "description",
+ itemType: "Workflow",
+ initialFilter: "mine",
+ });
return (
- <>
-
-
-
- {!projectListRef.current &&
}
- {projectListRef.current && isEmpty(projectListRef?.current) && (
-
- {
- showNewProject();
- toggleModal(true);
- }}
- />
-
- )}
- {isEmpty(projectList) && !isEmpty(projectListRef?.current) && (
-
-
No results found for this search
-
- )}
- {!isEmpty(projectList) && (
-
- )}
- {editingProject && (
-
import("../new-workflow/NewWorkflow.jsx")}
- componentName={"NewWorkflow"}
- name={editingProject.name}
- description={editingProject.description}
- onDone={editProject}
- onClose={closeNewProject}
- loading={loading}
- toggleModal={toggleModal}
- openModal={openModal}
- backendErrors={backendErrors}
- setBackendErrors={setBackendErrors}
- />
- )}
-
-
- >
+ }
+ itemProps={{
+ titleProp: "workflow_name",
+ descriptionProp: "description",
+ idProp: "id",
+ type: "Workflow",
+ }}
+ setPostHogCustomEvent={setPostHogCustomEvent}
+ newButtonEventName="intent_new_wf_project"
+ />
);
}
-function User({ name }) {
- return name ? (
-
-
-
- {name}
-
-
- ) : null;
-}
-
-User.propTypes = {
- name: PropTypes.string,
-};
-
export { Workflows };
diff --git a/frontend/src/components/workflows/workflow/workflow-service.js b/frontend/src/components/workflows/workflow/workflow-service.js
index 494efe4b2..c3c1e12b1 100644
--- a/frontend/src/components/workflows/workflow/workflow-service.js
+++ b/frontend/src/components/workflows/workflow/workflow-service.js
@@ -24,12 +24,10 @@ function workflowService() {
};
return axiosPrivate(options);
},
- getProjectList: (myProjects = false) => {
- const params = myProjects ? { created_by: sessionDetails?.id } : {};
+ getProjectList: () => {
options = {
url: `${path}/workflow/`,
method: "GET",
- params,
};
return axiosPrivate(options);
},
diff --git a/frontend/src/hooks/useListManager.js b/frontend/src/hooks/useListManager.js
new file mode 100644
index 000000000..c06707e90
--- /dev/null
+++ b/frontend/src/hooks/useListManager.js
@@ -0,0 +1,150 @@
+import { useState, useEffect } from "react";
+import { useExceptionHandler } from "./useExceptionHandler";
+import { useAlertStore } from "../store/alert-store";
+import { useSessionStore } from "../store/session-store";
+import { useAxiosPrivate } from "./useAxiosPrivate";
+
+export function useListManager({
+ getListApiCall,
+ addItemApiCall,
+ editItemApiCall,
+ deleteItemApiCall,
+ searchProperty = "",
+ itemIdProp = "id",
+ itemType = "item",
+ initialFilter,
+ onAddSuccess,
+ onEditSuccess,
+ onDeleteSuccess,
+ onError,
+}) {
+ const [list, setList] = useState([]);
+ const [filteredList, setFilteredList] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ const handleException = useExceptionHandler();
+ const { setAlertDetails } = useAlertStore();
+ const sessionDetails = useSessionStore((state) => state?.sessionDetails);
+ const axiosPrivate = useAxiosPrivate();
+
+ useEffect(() => {
+ fetchList();
+ }, [initialFilter]);
+
+ const fetchList = () => {
+ if (!getListApiCall) return;
+
+ setLoading(true);
+ getListApiCall({ axiosPrivate, sessionDetails, initialFilter })
+ .then((res) => {
+ const data = res?.data || [];
+ setList(data);
+ setFilteredList(data);
+ })
+ .catch((err) => {
+ const errorMsg = handleException(
+ err,
+ `Failed to get the list of ${itemType}s`
+ );
+ setAlertDetails(errorMsg);
+ onError?.(err);
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ };
+
+ const updateList = (itemData, itemId, isEdit = false) => {
+ let updatedList = [];
+
+ if (isEdit) {
+ updatedList = list.map((item) =>
+ item?.[itemIdProp] === itemId ? itemData : item
+ );
+ onEditSuccess?.(itemData);
+ } else {
+ updatedList = [itemData, ...list];
+ onAddSuccess?.(itemData);
+ }
+
+ setList(updatedList);
+ setFilteredList(updatedList);
+ };
+
+ const handleSearch = (searchText = "") => {
+ if (!searchText.trim()) {
+ setFilteredList(list);
+ return;
+ }
+ const filtered = list.filter((item) =>
+ item?.[searchProperty]?.toLowerCase()?.includes(searchText.toLowerCase())
+ );
+ setFilteredList(filtered);
+ };
+
+ const handleAddItem = (itemData, itemId, isEdit = false) => {
+ const apiCall = isEdit ? editItemApiCall : addItemApiCall;
+ if (!apiCall) return Promise.reject(new Error("API call is not defined"));
+
+ return apiCall({ axiosPrivate, sessionDetails, itemData, itemId })
+ .then((res) => {
+ const updatedItem = res?.data;
+ let updatedList = [];
+
+ if (isEdit) {
+ updatedList = list.map((item) =>
+ item?.[itemIdProp] === itemId ? updatedItem : item
+ );
+ onEditSuccess?.(updatedItem);
+ } else {
+ updatedList = [...list, updatedItem];
+ onAddSuccess?.(updatedItem);
+ }
+
+ setList(updatedList);
+ setFilteredList(updatedList);
+ return updatedItem;
+ })
+ .catch((err) => {
+ const errorMsg = handleException(
+ err,
+ `Failed to ${isEdit ? "edit" : "add"} ${itemType}`
+ );
+ setAlertDetails(errorMsg);
+ onError?.(err);
+ throw err;
+ });
+ };
+
+ const handleDeleteItem = (itemId) => {
+ if (!deleteItemApiCall)
+ return Promise.reject(new Error("API call is not defined"));
+
+ return deleteItemApiCall({ axiosPrivate, sessionDetails, itemId })
+ .then(() => {
+ const updatedList = list.filter(
+ (item) => item?.[itemIdProp] !== itemId
+ );
+ setList(updatedList);
+ setFilteredList(updatedList);
+ onDeleteSuccess?.(itemId);
+ })
+ .catch((err) => {
+ const errorMsg = handleException(err, `Failed to delete ${itemType}`);
+ setAlertDetails(errorMsg);
+ onError?.(err);
+ throw err;
+ });
+ };
+
+ return {
+ list,
+ filteredList,
+ updateList,
+ loading,
+ fetchList,
+ handleSearch,
+ handleAddItem,
+ handleDeleteItem,
+ };
+}