Skip to content

Commit

Permalink
dataframe table design improved (#1227)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahrukh802 authored Jun 13, 2024
1 parent 4cc3579 commit e105287
Show file tree
Hide file tree
Showing 14 changed files with 184 additions and 3,437 deletions.
3 changes: 2 additions & 1 deletion client/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"react/no-unescaped-entities": ["error", { "forbid": [">", "}"] }],
"@typescript-eslint/no-explicit-any": "off",
"no-useless-catch": "off",
"react-hooks/exhaustive-deps": 0
"react-hooks/exhaustive-deps": 0,
"react/prop-types": "off"
}
}
1 change: 0 additions & 1 deletion client/Providers/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import "styles/globals.css";
import "styles/App.css";
import "styles/multi-range-slider.css";
import "react-toastify/dist/ReactToastify.css";
import "react-tooltip/dist/react-tooltip.css";

const _NoSSR = ({ children }) => <React.Fragment>{children}</React.Fragment>;
const NoSSR = dynamic(() => Promise.resolve(_NoSSR), {
Expand Down
18 changes: 8 additions & 10 deletions client/components/AddUserModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useAddSpaceUsers, useDeleteSpaceUsers } from "hooks/useSpaces";
import { Loader } from "components/loader/Loader";
import { toast } from "react-toastify";
import { useGetMembersList, useGetOrganizations } from "hooks/useOrganizations";
import { Tooltip } from "react-tooltip";
import { Input } from "../ui/input";
import AppTooltip from "../AppTooltip";

interface ISpaceUser {
space_id: string;
Expand Down Expand Up @@ -111,15 +111,13 @@ const AddUserModal = ({ setIsModelOpen, spaceUsers, spaceId }: IProps) => {
className="cursor-pointer min-w-[50px] border-white/0 py-3 pr-4 pl-4 flex"
onClick={() => handleDeleteSpaceUser(user?.user_id)}
>
<AiFillDelete id="deleteIcon" color="#ccc" size="1.5em" />

<Tooltip
anchorSelect="#deleteIcon"
className="z-10"
opacity={1}
>
You cannot remove the sole user from this workspace
</Tooltip>
<AppTooltip text="You cannot remove the sole user from this workspace">
<AiFillDelete
id="deleteIcon"
color="#ccc"
size="1.5em"
/>
</AppTooltip>
</span>
) : (
<span
Expand Down
28 changes: 28 additions & 0 deletions client/components/AppTooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";
import React from "react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";

interface IProps {
text: string;
children: React.ReactNode;
}

const AppTooltip = ({ text, children }: IProps) => {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipContent>
<p>{text}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

export default AppTooltip;
1 change: 0 additions & 1 deletion client/components/ChatScreen/AIChatBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ const AIChatBubble = ({ chat, lastIndex }: IProps) => {
<ChatDataFrame
chatResponse={response}
chatId={chat.id}
index={indx}
key={chat?.id}
/>
</>
Expand Down
237 changes: 57 additions & 180 deletions client/components/ChatScreen/ChatDataFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,202 +1,79 @@
"use client";
import { FetchDataframe } from "services/chat";
import { ChatResponseItem } from "@/types/chat-types";
import React, { useState, useRef, useEffect } from "react";
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
import { toast } from "react-toastify";
import { Grid } from "gridjs";
import DownloadIcon from "../Icons/DownloadIcon";
import SearchIcon from "../Icons/SearchIcon";
import { useAppStore } from "@/store";
import AppTooltip from "../AppTooltip";
import "gridjs/dist/theme/mermaid.css";
import { convertToCSV } from "@/utils/convertToCSV";

interface IProps {
chatResponse: ChatResponseItem;
index: number;
chatId: string;
}

const StyledRecord = ({ record }: { record: string }) => {
if (!record) return record;

if (record === "true" || record === "True") {
return (
<span className="text-green-500">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</span>
);
} else if (record === "false" || record === "False") {
return (
<span className="text-red-500">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</span>
);
} else if (record.includes("http") || record.includes("www")) {
return (
<a
href={record}
target="_blank"
rel="noreferrer noopener"
className="text-[#86ade4] hover:underline"
>
{record}
</a>
);
} else if (record.includes("@") && record.includes(".")) {
return (
<a href={`mailto:${record}`} className="text-[#86ade4] hover:underline">
{record}
</a>
);
} else {
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (dateRegex.test(record)) {
const date = new Date(record);
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
};
return date.toLocaleDateString("en-US", options);
} else {
return record;
}
}
};

const ChatDataFrame = ({ chatResponse, index, chatId }: IProps) => {
const [expandedCardId, setExpandedCardId] = useState(null);
const [contentHeight, setContentHeight] = useState("300px");
const [isDownloading, setIsDownloading] = useState(false);
const ChatDataFrame = ({ chatResponse, chatId }: IProps) => {
const contentRef = useRef(null);
const [search, setSearch] = useState(false);
const darkMode = useAppStore((state) => state.darkMode);

const grid = new Grid({
columns: chatResponse?.value?.headers,
data: chatResponse?.value?.rows,
sort: true,
search: true,
pagination: {
limit: 10,
summary: false,
},
resizable: true,
});

useEffect(() => {
if (expandedCardId === index && contentRef.current) {
setContentHeight(`${contentRef.current.scrollHeight}px`);
} else {
setContentHeight("300px");
}
}, [expandedCardId, index]);
grid.render(contentRef.current);
});

const handleSearch = () => {
// const gridHead = document.querySelector(".gridjs-head") as HTMLElement;
const gridJsHead = contentRef.current.querySelector(".gridjs-head");

const handleToggleExpand = (index) => {
setExpandedCardId((prevId) => (prevId === index ? null : index));
if (gridJsHead) {
gridJsHead.style.display = search ? "none" : "block";
setSearch(!search);
}
};

const handleDownload = async (id: string) => {
setIsDownloading(true);
await FetchDataframe(id)
.then((response) => {
const fileUrl = response?.data?.data?.download_url;
const link = document.createElement("a");
link.href = fileUrl;
link.setAttribute("download", `dataframe-${id}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch((error) => {
toast.error(
error?.response?.data?.message
? error.response.data.message
: error.message
);
})
.finally(() => setIsDownloading(false));
const handleDownload = async () => {
const { headers, rows } = chatResponse.value;
const csvData = convertToCSV(headers, rows);
const blob = new Blob([csvData], { type: "text/csv" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.setAttribute("href", url);
a.setAttribute("download", `dataframe-${chatId}.csv`);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};

return (
<div className="mt-2">
<div
ref={contentRef}
className={`relative bg-white dark:bg-[#333333] rounded-[20px] px-4 py-2 custom-scroll overflow-hidden overflow-x-auto transition-all`}
style={{ maxHeight: contentHeight, height: "auto" }}
>
<table className="overflow-auto w-full">
<thead>
<tr className="!border-px !border-gray-400 uppercase p-2">
{chatResponse?.value?.headers?.map((item, index) => (
<th
key={index}
className="cursor-pointer border-b p-2 text-center border-solid border-r last:border-r-0 text-xs min-w-[100px]"
>
{item.split("_").join(" ")}
</th>
))}
</tr>
</thead>
<tbody>
{chatResponse?.value?.rows?.map((rows, index) => (
<tr className="cursor-pointer" key={index}>
{Object.values(rows).map((item, index) => (
<td
key={index}
className="text-center border-solid border-r dark:border-white border-[rgba(0,0,0,0.10)] last:border-r-0 pt-3"
>
<StyledRecord record={`${item}`} />
</td>
))}
</tr>
))}
</tbody>
</table>
{chatResponse?.value?.rows?.length > 3 && (
<div
className={`absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-white dark:from-[#191919] to-transparent pointer-events-none ${
expandedCardId !== index ? "opacity-100" : "opacity-0"
} transition-all`}
></div>
)}

{expandedCardId === index && (
<div className="text-center mt-6">
<button
className="cursor-pointer bg-[#191919] h-[24px] text-xs text-white font-bold rounded-full px-3 py-1"
onClick={() => {
handleDownload(chatId);
}}
>
{isDownloading ? "Downloading..." : "Download the full table"}
</button>
</div>
)}
</div>
<div className="text-center mt-2 flex flex-col gap-2 items-center justify-center">
{chatResponse?.value?.rows?.length > 3 && (
<button
className="h-6 w-6 flex items-center justify-center text-white bg-[#191919] rounded-full"
onClick={() => handleToggleExpand(index)}
>
{expandedCardId === index ? (
<FaChevronUp size="1em" />
) : (
<FaChevronDown size="1em" />
)}
</button>
)}
<div className="flex flex-col mt-2">
<div className="flex justify-end">
<div className="cursor-pointer" onClick={handleSearch}>
<AppTooltip text="Search">
<SearchIcon color={darkMode ? "#fff" : "#000"} />
</AppTooltip>
</div>
<div className="cursor-pointer" onClick={handleDownload}>
<AppTooltip text="Download">
<DownloadIcon color={darkMode ? "#fff" : "#000"} />
</AppTooltip>
</div>
</div>
<div ref={contentRef} className={`grid-container${chatId}`} />
</div>
);
};
Expand Down
20 changes: 20 additions & 0 deletions client/components/Icons/DownloadIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";

const DownloadIcon = ({ color = "white" }: { color?: string }) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15.577L8.461 12.039L9.169 11.319L11.5 13.65V5H12.5V13.65L14.83 11.32L15.539 12.039L12 15.577ZM6.616 19C6.15533 19 5.771 18.846 5.463 18.538C5.155 18.23 5.00067 17.8453 5 17.384V14.961H6V17.384C6 17.538 6.064 17.6793 6.192 17.808C6.32 17.9367 6.461 18.0007 6.615 18H17.385C17.5383 18 17.6793 17.936 17.808 17.808C17.9367 17.68 18.0007 17.5387 18 17.384V14.961H19V17.384C19 17.8447 18.846 18.229 18.538 18.537C18.23 18.845 17.8453 18.9993 17.384 19H6.616Z"
fill={color}
/>
</svg>
);
};

export default DownloadIcon;
20 changes: 20 additions & 0 deletions client/components/Icons/SearchIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";

const DownloadIcon = ({ color = "white" }: { color?: string }) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.0259 13.8476L18.5951 17.4159L17.4159 18.5951L13.8476 15.0259C12.5199 16.0903 10.8684 16.6692 9.16675 16.6667C5.02675 16.6667 1.66675 13.3067 1.66675 9.16675C1.66675 5.02675 5.02675 1.66675 9.16675 1.66675C13.3067 1.66675 16.6667 5.02675 16.6667 9.16675C16.6692 10.8684 16.0903 12.5199 15.0259 13.8476ZM13.3542 13.2292C14.4118 12.1417 15.0025 10.6838 15.0001 9.16675C15.0001 5.94425 12.3892 3.33341 9.16675 3.33341C5.94425 3.33341 3.33341 5.94425 3.33341 9.16675C3.33341 12.3892 5.94425 15.0001 9.16675 15.0001C10.6838 15.0025 12.1417 14.4118 13.2292 13.3542L13.3542 13.2292Z"
fill={color}
/>
</svg>
);
};

export default DownloadIcon;
Loading

0 comments on commit e105287

Please sign in to comment.