From 0fc48ea03f619db5b2f0921ca8f65850ca6c4c95 Mon Sep 17 00:00:00 2001 From: Prathamesh Gursal Date: Sun, 6 Oct 2024 11:13:01 +0530 Subject: [PATCH] applying loader in the settings respective components --- frontend/src/settings/APIKeys.tsx | 97 ++++++++------- frontend/src/settings/Analytics.tsx | 185 ++++++++++++++++------------ frontend/src/settings/Documents.tsx | 156 ++++++++++++----------- frontend/src/settings/Logs.tsx | 105 +++++++++------- frontend/src/utils/loader.tsx | 13 +- 5 files changed, 319 insertions(+), 237 deletions(-) diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index ebb32268..c108f93a 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -1,20 +1,23 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import userService from '../api/services/userService'; import Trash from '../assets/trash.svg'; import CreateAPIKeyModal from '../modals/CreateAPIKeyModal'; import SaveAPIKeyModal from '../modals/SaveAPIKeyModal'; +import SkeletonLoader from '../utils/loader'; import { APIKeyData } from './types'; export default function APIKeys() { const { t } = useTranslation(); - const [isCreateModalOpen, setCreateModal] = React.useState(false); - const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false); - const [newKey, setNewKey] = React.useState(''); - const [apiKeys, setApiKeys] = React.useState([]); + const [isCreateModalOpen, setCreateModal] = useState(false); + const [isSaveKeyModalOpen, setSaveKeyModal] = useState(false); + const [newKey, setNewKey] = useState(''); + const [apiKeys, setApiKeys] = useState([]); + const [loading, setLoading] = useState(true); const handleFetchKeys = async () => { + setLoading(true); try { const response = await userService.getAPIKeys(); if (!response.ok) { @@ -24,6 +27,8 @@ export default function APIKeys() { setApiKeys(apiKeys); } catch (error) { console.log(error); + } finally { + setLoading(false); } }; @@ -72,9 +77,10 @@ export default function APIKeys() { }); }; - React.useEffect(() => { + useEffect(() => { handleFetchKeys(); }, []); + return (
@@ -98,44 +104,51 @@ export default function APIKeys() { close={() => setSaveKeyModal(false)} /> )} -
-
- - - - - - - - - - - {apiKeys?.map((element, index) => ( - - - - - + + {loading ? ( + + ) : ( +
+
+
- {t('settings.apiKeys.name')} - - {t('settings.apiKeys.sourceDoc')} - - {t('settings.apiKeys.key')} -
{element.name}{element.source}{element.key} - Delete handleDeleteKey(element.id)} - /> -
+ + + + + + - ))} - -
+ {t('settings.apiKeys.name')} + + {t('settings.apiKeys.sourceDoc')} + + {t('settings.apiKeys.key')} +
+ + + {apiKeys?.map((element, index) => ( + + {element.name} + + {element.source} + + {element.key} + + Delete handleDeleteKey(element.id)} + /> + + + ))} + + +
-
+ )}
); diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index 5ddab2cb..2d22ab33 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -1,3 +1,4 @@ +import React, { useState, useEffect } from 'react'; import { BarElement, CategoryScale, @@ -7,7 +8,6 @@ import { Title, Tooltip, } from 'chart.js'; -import React from 'react'; import { Bar } from 'react-chartjs-2'; import userService from '../api/services/userService'; @@ -17,6 +17,7 @@ import { formatDate } from '../utils/dateTimeUtils'; import { APIKeyData } from './types'; import type { ChartData } from 'chart.js'; +import SkeletonLoader from '../utils/loader'; ChartJS.register( CategoryScale, LinearScale, @@ -35,37 +36,37 @@ const filterOptions = [ ]; export default function Analytics() { - const [messagesData, setMessagesData] = React.useState | null>(null); - const [tokenUsageData, setTokenUsageData] = React.useState | null>(null); - const [feedbackData, setFeedbackData] = React.useState | null>(null); - const [chatbots, setChatbots] = React.useState([]); - const [selectedChatbot, setSelectedChatbot] = - React.useState(); - const [messagesFilter, setMessagesFilter] = React.useState<{ + const [chatbots, setChatbots] = useState([]); + const [selectedChatbot, setSelectedChatbot] = useState(); + const [messagesFilter, setMessagesFilter] = useState<{ label: string; value: string; }>({ label: '30 Days', value: 'last_30_days' }); - const [tokenUsageFilter, setTokenUsageFilter] = React.useState<{ + const [tokenUsageFilter, setTokenUsageFilter] = useState<{ label: string; value: string; }>({ label: '30 Days', value: 'last_30_days' }); - const [feedbackFilter, setFeedbackFilter] = React.useState<{ + const [feedbackFilter, setFeedbackFilter] = useState<{ label: string; value: string; }>({ label: '30 Days', value: 'last_30_days' }); + const [loadingMessages, setLoadingMessages] = useState(true); // Loading state for messages + const [loadingTokens, setLoadingTokens] = useState(true); // Loading state for tokens + const [loadingFeedback, setLoadingFeedback] = useState(true); // Loading state for feedback + const fetchChatbots = async () => { try { const response = await userService.getAPIKeys(); @@ -80,6 +81,7 @@ export default function Analytics() { }; const fetchMessagesData = async (chatbot_id?: string, filter?: string) => { + setLoadingMessages(true); // Start loading try { const response = await userService.getMessageAnalytics({ api_key_id: chatbot_id, @@ -92,10 +94,13 @@ export default function Analytics() { setMessagesData(data.messages); } catch (error) { console.error(error); + } finally { + setLoadingMessages(false); // Stop loading } }; const fetchTokenData = async (chatbot_id?: string, filter?: string) => { + setLoadingTokens(true); // Start loading try { const response = await userService.getTokenAnalytics({ api_key_id: chatbot_id, @@ -108,10 +113,13 @@ export default function Analytics() { setTokenUsageData(data.token_usage); } catch (error) { console.error(error); + } finally { + setLoadingTokens(false); // Stop loading } }; const fetchFeedbackData = async (chatbot_id?: string, filter?: string) => { + setLoadingFeedback(true); // Start loading try { const response = await userService.getFeedbackAnalytics({ api_key_id: chatbot_id, @@ -124,30 +132,33 @@ export default function Analytics() { setFeedbackData(data.feedback); } catch (error) { console.error(error); + } finally { + setLoadingFeedback(false); } }; - React.useEffect(() => { + useEffect(() => { fetchChatbots(); }, []); - React.useEffect(() => { + useEffect(() => { const id = selectedChatbot?.id; const filter = messagesFilter; fetchMessagesData(id, filter?.value); }, [selectedChatbot, messagesFilter]); - React.useEffect(() => { + useEffect(() => { const id = selectedChatbot?.id; const filter = tokenUsageFilter; fetchTokenData(id, filter?.value); }, [selectedChatbot, tokenUsageFilter]); - React.useEffect(() => { + useEffect(() => { const id = selectedChatbot?.id; const filter = feedbackFilter; fetchFeedbackData(id, filter?.value); }, [selectedChatbot, feedbackFilter]); + return (
@@ -181,6 +192,8 @@ export default function Analytics() { border="border" />
+ + {/* Messages Analytics */}
@@ -208,25 +221,31 @@ export default function Analytics() { id="legend-container-1" className="flex flex-row items-center justify-end" >
- - formatDate(item), - ), - datasets: [ - { - label: 'Messages', - data: Object.values(messagesData || {}), - backgroundColor: '#7D54D1', - }, - ], - }} - legendID="legend-container-1" - maxTicksLimitInX={8} - isStacked={false} - /> + {loadingMessages ? ( + + ) : ( + + formatDate(item), + ), + datasets: [ + { + label: 'Messages', + data: Object.values(messagesData || {}), + backgroundColor: '#7D54D1', + }, + ], + }} + legendID="legend-container-1" + maxTicksLimitInX={8} + isStacked={false} + /> + )}
+ + {/* Token Usage Analytics */}

@@ -253,26 +272,32 @@ export default function Analytics() { id="legend-container-2" className="flex flex-row items-center justify-end" >

- - formatDate(item), - ), - datasets: [ - { - label: 'Tokens', - data: Object.values(tokenUsageData || {}), - backgroundColor: '#7D54D1', - }, - ], - }} - legendID="legend-container-2" - maxTicksLimitInX={8} - isStacked={false} - /> + {loadingTokens ? ( + + ) : ( + + formatDate(item), + ), + datasets: [ + { + label: 'Tokens', + data: Object.values(tokenUsageData || {}), + backgroundColor: '#7D54D1', + }, + ], + }} + legendID="legend-container-2" + maxTicksLimitInX={8} + isStacked={false} + /> + )}
+ + {/* Feedback Analytics */}
@@ -300,32 +325,36 @@ export default function Analytics() { id="legend-container-3" className="flex flex-row items-center justify-end" >
- - formatDate(item), - ), - datasets: [ - { - label: 'Positive', - data: Object.values(feedbackData || {}).map( - (item) => item.positive, - ), - backgroundColor: '#8BD154', - }, - { - label: 'Negative', - data: Object.values(feedbackData || {}).map( - (item) => item.negative, - ), - backgroundColor: '#D15454', - }, - ], - }} - legendID="legend-container-3" - maxTicksLimitInX={10} - isStacked={true} - /> + {loadingFeedback ? ( + + ) : ( + + formatDate(item), + ), + datasets: [ + { + label: 'Positive', + data: Object.values(feedbackData || {}).map( + (item) => item.positive, + ), + backgroundColor: '#8BD154', + }, + { + label: 'Negative', + data: Object.values(feedbackData || {}).map( + (item) => item.negative, + ), + backgroundColor: '#D15454', + }, + ], + }} + legendID="legend-container-3" + maxTicksLimitInX={10} + isStacked={true} + /> + )}
diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index ee88a98f..6de3311a 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -1,7 +1,7 @@ +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; - import userService from '../api/services/userService'; import SyncIcon from '../assets/sync.svg'; import Trash from '../assets/trash.svg'; @@ -9,8 +9,8 @@ import DropdownMenu from '../components/DropdownMenu'; import { Doc, DocumentsProps } from '../models/misc'; import { getDocs } from '../preferences/preferenceApi'; import { setSourceDocs } from '../preferences/preferenceSlice'; +import SkeletonLoader from '../utils/loader'; -// Utility function to format numbers const formatTokens = (tokens: number): string => { const roundToTwoDecimals = (num: number): string => { return (Math.round((num + Number.EPSILON) * 100) / 100).toString(); @@ -33,6 +33,9 @@ const Documents: React.FC = ({ }) => { const { t } = useTranslation(); const dispatch = useDispatch(); + + const [loading, setLoading] = useState(false); + const syncOptions = [ { label: 'Never', value: 'never' }, { label: 'Daily', value: 'daily' }, @@ -41,6 +44,7 @@ const Documents: React.FC = ({ ]; const handleManageSync = (doc: Doc, sync_frequency: string) => { + setLoading(true); userService .manageSync({ source_id: doc.id, sync_frequency }) .then(() => { @@ -49,86 +53,96 @@ const Documents: React.FC = ({ .then((data) => { dispatch(setSourceDocs(data)); }) - .catch((error) => console.error(error)); + .catch((error) => console.error(error)) + .finally(() => setLoading(false)); }; + return (
-
-
- - - - - - - - - - - - {documents && - documents.map((document, index) => ( - - - - - - + + ))} + +
- {t('settings.documents.name')} - - {t('settings.documents.date')} - - {t('settings.documents.tokenUsage')} - - {t('settings.documents.type')} -
- {document.name} - - {document.date} - - {document.tokens ? formatTokens(+document.tokens) : ''} - - {document.type === 'remote' ? 'Pre-loaded' : 'Private'} - -
- {document.type !== 'remote' && ( - Delete { - event.stopPropagation(); - handleDeleteDocument(index, document); - }} - /> - )} - {document.syncFrequency && ( -
- { - handleManageSync(document, value); + {loading ? ( + + ) : ( +
+
+ + + + + + + + + + + + {documents && + documents.map((document, index) => ( + + + + + + - - ))} - -
+ {t('settings.documents.name')} + + {t('settings.documents.date')} + + {t('settings.documents.tokenUsage')} + + {t('settings.documents.type')} +
+ {document.name} + + {document.date} + + {document.tokens ? formatTokens(+document.tokens) : ''} + + {document.type === 'remote' ? 'Pre-loaded' : 'Private'} + +
+ {document.type !== 'remote' && ( + Delete { + event.stopPropagation(); + setLoading(true); + handleDeleteDocument(index, document); + setLoading(false); }} - defaultValue={document.syncFrequency} - icon={SyncIcon} /> -
- )} - -
+ )} + {document.syncFrequency && ( +
+ { + handleManageSync(document, value); + }} + defaultValue={document.syncFrequency} + icon={SyncIcon} + /> +
+ )} +
+
+
-
+ )} ); }; + Documents.propTypes = { documents: PropTypes.array.isRequired, handleDeleteDocument: PropTypes.func.isRequired, }; + export default Documents; diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index 58ab930d..32d89564 100644 --- a/frontend/src/settings/Logs.tsx +++ b/frontend/src/settings/Logs.tsx @@ -1,20 +1,23 @@ -import React from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import userService from '../api/services/userService'; import ChevronRight from '../assets/chevron-right.svg'; import Dropdown from '../components/Dropdown'; +import SkeletonLoader from '../utils/loader'; import { APIKeyData, LogData } from './types'; import CoppyButton from '../components/CopyButton'; export default function Logs() { - const [chatbots, setChatbots] = React.useState([]); - const [selectedChatbot, setSelectedChatbot] = - React.useState(); - const [logs, setLogs] = React.useState([]); - const [page, setPage] = React.useState(1); - const [hasMore, setHasMore] = React.useState(true); + const [chatbots, setChatbots] = useState([]); + const [selectedChatbot, setSelectedChatbot] = useState(); + const [logs, setLogs] = useState([]); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [loadingChatbots, setLoadingChatbots] = useState(true); + const [loadingLogs, setLoadingLogs] = useState(true); const fetchChatbots = async () => { + setLoadingChatbots(true); try { const response = await userService.getAPIKeys(); if (!response.ok) { @@ -24,10 +27,13 @@ export default function Logs() { setChatbots(chatbots); } catch (error) { console.error(error); + } finally { + setLoadingChatbots(false); } }; const fetchLogs = async () => { + setLoadingLogs(true); try { const response = await userService.getLogs({ page: page, @@ -38,20 +44,23 @@ export default function Logs() { throw new Error('Failed to fetch logs'); } const olderLogs = await response.json(); - setLogs([...logs, ...olderLogs.logs]); + setLogs((prevLogs) => [...prevLogs, ...olderLogs.logs]); setHasMore(olderLogs.has_more); } catch (error) { console.error(error); + } finally { + setLoadingLogs(false); } }; - React.useEffect(() => { + useEffect(() => { fetchChatbots(); }, []); - React.useEffect(() => { + useEffect(() => { if (hasMore) fetchLogs(); }, [page, selectedChatbot]); + return (
@@ -59,38 +68,47 @@ export default function Logs() {

Filter by chatbot

- ({ - label: chatbot.name, - value: chatbot.id, - })), - { label: 'None', value: '' }, - ]} - placeholder="Select chatbot" - onSelect={(chatbot: { label: string; value: string }) => { - setSelectedChatbot( - chatbots.find((item) => item.id === chatbot.value), - ); - setLogs([]); - setPage(1); - setHasMore(true); - }} - selectedValue={ - (selectedChatbot && { - label: selectedChatbot.name, - value: selectedChatbot.id, - }) || - null - } - rounded="3xl" - border="border" - /> + {loadingChatbots ? ( + + ) : ( + ({ + label: chatbot.name, + value: chatbot.id, + })), + { label: 'None', value: '' }, + ]} + placeholder="Select chatbot" + onSelect={(chatbot: { label: string; value: string }) => { + setSelectedChatbot( + chatbots.find((item) => item.id === chatbot.value), + ); + setLogs([]); + setPage(1); + setHasMore(true); + }} + selectedValue={ + (selectedChatbot && { + label: selectedChatbot.name, + value: selectedChatbot.id, + }) || + null + } + rounded="3xl" + border="border" + /> + )}
+
- + {loadingLogs ? ( + + ) : ( + + )}
); @@ -102,15 +120,16 @@ type LogsTableProps = { }; function LogsTable({ logs, setPage }: LogsTableProps) { - const observerRef = React.useRef(); - const firstObserver = React.useCallback((node: HTMLDivElement) => { + const observerRef = useRef(); + const firstObserver = useCallback((node: HTMLDivElement) => { if (observerRef.current) { - observerRef.current = new IntersectionObserver((enteries) => { - if (enteries[0].isIntersecting) setPage((prev) => prev + 1); + observerRef.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) setPage((prev) => prev + 1); }); } if (node && observerRef.current) observerRef.current.observe(node); }, []); + return (
diff --git a/frontend/src/utils/loader.tsx b/frontend/src/utils/loader.tsx index 0d579f85..bbf8ba25 100644 --- a/frontend/src/utils/loader.tsx +++ b/frontend/src/utils/loader.tsx @@ -1,10 +1,17 @@ import React from 'react'; -const SkeletonLoader = ({ count = 1 }) => { +interface SkeletonLoaderProps { + count?: number; // Optional prop to define the number of skeleton loaders +} + +const SkeletonLoader: React.FC = ({ count = 1 }) => { return (
{[...Array(count)].map((_, idx) => ( -
+
@@ -18,5 +25,5 @@ const SkeletonLoader = ({ count = 1 }) => { export default SkeletonLoader; -// calling function should be pass --- no. of sketeton cards +// calling function should be pass --- no. of sketeton cards // eg . ----------->>>