-
Notifications
You must be signed in to change notification settings - Fork 486
FEAT: Optimize List View Components with Table Design and Sorting Across Project Pages #819
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tahierhussain
wants to merge
8
commits into
main
Choose a base branch
from
feat/sort-list-view-projects
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,109
−944
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
0ee46bf
Refactor list view components for better performance across pages and…
tahierhussain 26b476d
Sonar issue fixes
tahierhussain 65f4b31
Merge branch 'main' of github.com:Zipstack/unstract into feat/sort-li…
tahierhussain ef88e85
Bug fixes in adapter settings pages
tahierhussain 0c8c7d3
Resolved sonar issues
tahierhussain 5ce71b7
Moved base url to a separate variable
tahierhussain b0dfc07
Moved from defaultProps to default parameters in ViewTools and ListVi…
tahierhussain ca03eed
Removed filter param for workflows API
tahierhussain File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
388 changes: 84 additions & 304 deletions
388
frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx
Large diffs are not rendered by default.
Oops, something went wrong.
35 changes: 35 additions & 0 deletions
35
frontend/src/components/custom-tools/list-of-tools/ListOfToolsModal.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<AddCustomToolFormModal | ||
open={open} | ||
setOpen={setOpen} | ||
editItem={editItem} | ||
isEdit={isEdit} | ||
handleAddNewTool={handleAddNewTool} | ||
/> | ||
); | ||
} | ||
|
||
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; |
38 changes: 21 additions & 17 deletions
38
frontend/src/components/custom-tools/view-tools/ViewTools.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 7 additions & 7 deletions
14
frontend/src/components/tool-settings/list-of-items/ListOfItems.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
362 changes: 168 additions & 194 deletions
362
frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx
Large diffs are not rendered by default.
Oops, something went wrong.
42 changes: 42 additions & 0 deletions
42
frontend/src/components/tool-settings/tool-settings/ToolSettingsModal.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<AddSourceModal | ||
open={open} | ||
setOpen={setOpen} | ||
type={type} | ||
addNewItem={handleAddNewItem} | ||
editItemId={editItem?.id} | ||
setEditItemId={() => {}} | ||
/> | ||
); | ||
} | ||
|
||
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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( | ||
() => ( | ||
<CustomButton | ||
type="primary" | ||
icon={customButtonIcon} | ||
onClick={handleCustomButtonClick} | ||
> | ||
{customButtonText} | ||
</CustomButton> | ||
), | ||
[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 ( | ||
<> | ||
<ToolNavBar | ||
title={title} | ||
enableSearch | ||
onSearch={handleSearch} | ||
searchList={list} | ||
setSearchList={filteredList} | ||
CustomButtons={CustomButtons} | ||
/> | ||
<div className="list-of-tools-layout"> | ||
<div className="list-of-tools-island"> | ||
<div className="list-of-tools-body"> | ||
<ViewTools | ||
isLoading={loading} | ||
isEmpty={!filteredList?.length} | ||
listOfTools={filteredList} | ||
setOpenAddTool={setOpenModal} | ||
handleEdit={handleEdit} | ||
handleDelete={handleDelete} | ||
{...itemProps} | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
{openModal && CustomModalComponent && ( | ||
<CustomModalComponent | ||
open={openModal} | ||
setOpen={setOpenModal} | ||
editItem={editingItem} | ||
isEdit={isEdit} | ||
handleAddItem={handleAddItem} | ||
updateList={updateList} | ||
type={type} | ||
/> | ||
)} | ||
</> | ||
); | ||
} | ||
|
||
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 }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
import { | ||
Avatar, | ||
Flex, | ||
Image, | ||
List, | ||
Popconfirm, | ||
Space, | ||
Table, | ||
Tooltip, | ||
Typography, | ||
} from "antd"; | ||
|
@@ -17,164 +17,245 @@ 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"; | ||
|
||
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 = ( | ||
<div className="adapter-cover-img"> | ||
<Image src={item[iconProp]} preview={false} className="fit-cover" /> | ||
const renderNameColumn = useCallback( | ||
(text, record) => { | ||
let titleContent = null; | ||
if (iconProp && record[iconProp]?.length > 4) { | ||
titleContent = ( | ||
<Space className="adapter-cover-img" size={10}> | ||
<Image | ||
src={record[iconProp]} | ||
preview={false} | ||
className="fit-cover" | ||
/> | ||
<Typography.Text className="adapters-list-title"> | ||
{record[titleProp]} | ||
</Typography.Text> | ||
</Space> | ||
); | ||
} else if (iconProp) { | ||
titleContent = ( | ||
<Typography.Text className="adapters-list-title"> | ||
{item[titleProp]} | ||
{`${record[iconProp]} ${record[titleProp]}`} | ||
</Typography.Text> | ||
</div> | ||
); | ||
} else if (iconProp) { | ||
title = ( | ||
<Typography.Text className="adapters-list-title"> | ||
{`${item[iconProp]} ${item[titleProp]}`} | ||
</Typography.Text> | ||
); | ||
} else { | ||
titleContent = ( | ||
<Typography.Text className="adapters-list-title"> | ||
{record[titleProp]} | ||
</Typography.Text> | ||
); | ||
} | ||
return ( | ||
<> | ||
<Typography.Text strong className="display-flex-left"> | ||
{titleContent} | ||
</Typography.Text> | ||
<Typography.Text type="secondary" ellipsis> | ||
<Tooltip title={record?.[descriptionProp]}> | ||
{record?.[descriptionProp]} | ||
</Tooltip> | ||
</Typography.Text> | ||
</> | ||
); | ||
} else { | ||
title = ( | ||
<Typography.Text className="adapters-list-title"> | ||
{item[titleProp]} | ||
</Typography.Text> | ||
}, | ||
[iconProp, titleProp, descriptionProp] | ||
); | ||
|
||
const renderOwnedByColumn = useCallback( | ||
(text) => { | ||
const owner = text === sessionDetails?.email ? "Me" : text || "-"; | ||
return ( | ||
<div className="adapters-list-profile-container"> | ||
<Avatar | ||
size={20} | ||
className="adapters-list-user-avatar" | ||
icon={<UserOutlined />} | ||
/> | ||
<Typography.Text disabled className="adapters-list-user-prefix"> | ||
Owned By: | ||
</Typography.Text> | ||
<Typography.Text className="shared-username">{owner}</Typography.Text> | ||
</div> | ||
); | ||
} | ||
}, | ||
[sessionDetails?.email] | ||
); | ||
|
||
const renderDateColumn = useCallback( | ||
(text) => (text ? moment(text).format("YYYY-MM-DD HH:mm:ss") : "-"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move the data formatter to static func? Are they reuseable? |
||
[] | ||
); | ||
|
||
const handleEditClick = useCallback( | ||
(event, record) => { | ||
event.stopPropagation(); | ||
handleEdit(event, record); | ||
}, | ||
[handleEdit] | ||
); | ||
|
||
const handleShareClick = useCallback( | ||
(event, record) => { | ||
event.stopPropagation(); | ||
handleShare?.(record, true); | ||
}, | ||
[handleShare] | ||
); | ||
|
||
return ( | ||
<Flex | ||
gap={20} | ||
align="center" | ||
justify="space-between" | ||
className="list-view-container" | ||
const handleDeleteClick = useCallback( | ||
(event, record) => { | ||
event.stopPropagation(); | ||
handleDelete(event, record); | ||
}, | ||
[handleDelete] | ||
); | ||
|
||
const renderActionsColumn = useCallback( | ||
(text, record) => ( | ||
<div | ||
className="action-button-container" | ||
onClick={(event) => event.stopPropagation()} | ||
role="none" | ||
> | ||
<div className="list-view-content"> | ||
<div className="adapters-list-title-container display-flex-left"> | ||
{title} | ||
</div> | ||
{showOwner && ( | ||
<div className="adapters-list-profile-container"> | ||
<Avatar | ||
size={20} | ||
className="adapters-list-user-avatar" | ||
icon={<UserOutlined />} | ||
/> | ||
<Typography.Text disabled className="adapters-list-user-prefix"> | ||
Owned By: | ||
</Typography.Text> | ||
<Typography.Text className="shared-username"> | ||
{item?.created_by_email | ||
? item?.created_by_email === sessionDetails.email | ||
? "Me" | ||
: item?.created_by_email | ||
: "-"} | ||
</Typography.Text> | ||
</div> | ||
)} | ||
</div> | ||
<div | ||
className="action-button-container" | ||
onClick={(event) => event.stopPropagation()} | ||
role="none" | ||
> | ||
<EditOutlined | ||
key={`${item.id}-edit`} | ||
onClick={(event) => handleEdit(event, item)} | ||
className="action-icon-buttons edit-icon" | ||
<EditOutlined | ||
key={`${record[idProp]}-edit`} | ||
onClick={(event) => handleEditClick(event, record)} | ||
className="action-icon-buttons edit-icon" | ||
/> | ||
{handleShare && ( | ||
<ShareAltOutlined | ||
key={`${record[idProp]}-share`} | ||
className="action-icon-buttons" | ||
onClick={(event) => handleShareClick(event, record)} | ||
/> | ||
{handleShare && ( | ||
<ShareAltOutlined | ||
key={`${item.id}-share`} | ||
className="action-icon-buttons" | ||
onClick={(event) => handleShareClick(event, item, true)} | ||
/> | ||
)} | ||
<Popconfirm | ||
key={`${item.id}-delete`} | ||
title={`Delete the ${type}`} | ||
description={`Are you sure to delete ${item[titleProp]}`} | ||
okText="Yes" | ||
cancelText="No" | ||
icon={<QuestionCircleOutlined />} | ||
onConfirm={(event) => { | ||
handleDeleteClick(event, item); | ||
}} | ||
> | ||
<Typography.Text> | ||
<DeleteOutlined className="action-icon-buttons delete-icon" /> | ||
</Typography.Text> | ||
</Popconfirm> | ||
</div> | ||
</Flex> | ||
); | ||
}; | ||
)} | ||
<Popconfirm | ||
key={`${record[idProp]}-delete`} | ||
title={`Delete the ${type}`} | ||
description={`Are you sure to delete ${record[titleProp]}?`} | ||
okText="Yes" | ||
cancelText="No" | ||
icon={<QuestionCircleOutlined />} | ||
onConfirm={(event) => { | ||
handleDeleteClick(event, record); | ||
}} | ||
> | ||
<Typography.Text> | ||
<DeleteOutlined className="action-icon-buttons delete-icon" /> | ||
</Typography.Text> | ||
</Popconfirm> | ||
</div> | ||
), | ||
[ | ||
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 ( | ||
<List | ||
size="large" | ||
<Table | ||
rowKey={idProp} | ||
dataSource={listOfTools} | ||
style={{ marginInline: "4px" }} | ||
columns={columns} | ||
pagination={{ | ||
position: "bottom", | ||
align: "end", | ||
position: ["bottomRight"], | ||
size: "small", | ||
}} | ||
className="list-view-wrapper" | ||
renderItem={(item) => { | ||
return ( | ||
<List.Item | ||
key={item?.id} | ||
onClick={(event) => { | ||
isClickable | ||
? navigate(`${item[idProp]}`) | ||
: handleShareClick(event, item, false); | ||
}} | ||
className={`cur-pointer ${centered ? "centered" : ""}`} | ||
> | ||
<List.Item.Meta | ||
className="list-item-desc" | ||
title={renderTitle(item)} | ||
description={ | ||
<Typography.Text type="secondary" ellipsis> | ||
<Tooltip title={item[descriptionProp]}> | ||
{item[descriptionProp]} | ||
</Tooltip> | ||
</Typography.Text> | ||
} | ||
></List.Item.Meta> | ||
</List.Item> | ||
); | ||
}} | ||
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, | ||
}; | ||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
frontend/src/components/workflows/workflow/WorkflowModal.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<LazyLoader | ||
component={() => 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; |
306 changes: 48 additions & 258 deletions
306
frontend/src/components/workflows/workflow/Workflows.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<CustomButton | ||
type="primary" | ||
icon={<PlusOutlined />} | ||
onClick={handleNewWorkflowBtnClick} | ||
> | ||
New Workflow | ||
</CustomButton> | ||
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 ( | ||
<> | ||
<ToolNavBar | ||
enableSearch | ||
searchList={projectList} | ||
setSearchList={setProjectList} | ||
CustomButtons={CustomButtons} | ||
segmentFilter={applyFilter} | ||
segmentOptions={PROJECT_FILTER_OPTIONS} | ||
onSearch={onSearch} | ||
/> | ||
<div className="workflows-pg-layout"> | ||
<div className="workflows-pg-body"> | ||
{!projectListRef.current && <SpinnerLoader />} | ||
{projectListRef.current && isEmpty(projectListRef?.current) && ( | ||
<div className="list-of-workflows-body"> | ||
<EmptyState | ||
text="No Workflow available" | ||
btnText="New Workflow" | ||
handleClick={() => { | ||
showNewProject(); | ||
toggleModal(true); | ||
}} | ||
/> | ||
</div> | ||
)} | ||
{isEmpty(projectList) && !isEmpty(projectListRef?.current) && ( | ||
<div className="center"> | ||
<Title level={5}>No results found for this search</Title> | ||
</div> | ||
)} | ||
{!isEmpty(projectList) && ( | ||
<ViewTools | ||
isLoading={loading} | ||
isEmpty={!projectList?.length} | ||
listOfTools={projectList} | ||
setOpenAddTool={toggleModal} | ||
handleEdit={updateProject} | ||
handleDelete={deleteProject} | ||
titleProp="workflow_name" | ||
descriptionProp="description" | ||
idProp="id" | ||
type="Workflow" | ||
/> | ||
)} | ||
{editingProject && ( | ||
<LazyLoader | ||
component={() => 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} | ||
/> | ||
)} | ||
</div> | ||
</div> | ||
</> | ||
<ListView | ||
title="Workflows" | ||
useListManagerHook={useListManagerHook} | ||
CustomModalComponent={WorkflowModal} | ||
customButtonText="New Workflow" | ||
customButtonIcon={<PlusOutlined />} | ||
itemProps={{ | ||
titleProp: "workflow_name", | ||
descriptionProp: "description", | ||
idProp: "id", | ||
type: "Workflow", | ||
}} | ||
setPostHogCustomEvent={setPostHogCustomEvent} | ||
newButtonEventName="intent_new_wf_project" | ||
/> | ||
); | ||
} | ||
|
||
function User({ name }) { | ||
return name ? ( | ||
<div className="sessionDetails"> | ||
<UserOutlined /> | ||
<Text italic ellipsis> | ||
{name} | ||
</Text> | ||
</div> | ||
) : null; | ||
} | ||
|
||
User.propTypes = { | ||
name: PropTypes.string, | ||
}; | ||
|
||
export { Workflows }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}; | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: Move props with default values to bottom for better code readability.