From db22b711d941ec6c83568587bea93a9233e78c59 Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 11 Nov 2024 02:14:44 +0100 Subject: [PATCH] tmp --- .attach_pid1264587 | 0 .github/workflows/deploy-frontend.yml | 5 +- src/frontend/.eslintrc.json | 5 +- src/frontend/app/MainPage.tsx | 83 ----- src/frontend/app/components/AutoComplete.tsx | 102 ------ .../app/components/BrowseResources.tsx | 294 ++++++++++++++++++ src/frontend/app/components/Modal.tsx | 44 +++ src/frontend/app/globals.css | 6 +- src/frontend/app/page.tsx | 4 +- src/frontend/app/utils/resources.ts | 73 +++++ src/frontend/app/utils/search.ts | 68 ++++ src/frontend/next.config.js | 1 - src/frontend/public/.nojekyll | 0 13 files changed, 494 insertions(+), 191 deletions(-) create mode 100644 .attach_pid1264587 delete mode 100644 src/frontend/app/MainPage.tsx delete mode 100644 src/frontend/app/components/AutoComplete.tsx create mode 100644 src/frontend/app/components/BrowseResources.tsx create mode 100644 src/frontend/app/components/Modal.tsx create mode 100644 src/frontend/app/utils/resources.ts create mode 100644 src/frontend/app/utils/search.ts create mode 100644 src/frontend/public/.nojekyll diff --git a/.attach_pid1264587 b/.attach_pid1264587 new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/deploy-frontend.yml b/.github/workflows/deploy-frontend.yml index 5c1d4f2..e681bfc 100644 --- a/.github/workflows/deploy-frontend.yml +++ b/.github/workflows/deploy-frontend.yml @@ -53,10 +53,13 @@ jobs: - name: Build with Next.js run: npm run build + - name: ls + run: ls -l + - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: ./out + path: /home/runner/work/api-gateway/api-gateway/src/frontend/out deploy: environment: diff --git a/src/frontend/.eslintrc.json b/src/frontend/.eslintrc.json index bffb357..13015d6 100644 --- a/src/frontend/.eslintrc.json +++ b/src/frontend/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "next/core-web-vitals" + "extends": "next/core-web-vitals", + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } } diff --git a/src/frontend/app/MainPage.tsx b/src/frontend/app/MainPage.tsx deleted file mode 100644 index cc3ccde..0000000 --- a/src/frontend/app/MainPage.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, - EuiPageHeader, - EuiPageHeaderSection, EuiBadge, EuiFieldText -} from '@elastic/eui'; -import Autocomplete from './components/AutoComplete' -import {useEuiPaddingCSS} from "@elastic/eui"; -import {EuiPanel} from "@elastic/eui"; -import {EuiHorizontalRule} from "@elastic/eui"; -import ArtefactsTable from "@/app/components/BrowseResources"; - - -function Header() { - return ( - - - ) -} - - - -export function MainPage(props: {apiUrl: string}) { - - const [apiUrlValue, setApiUrl] = useState(props.apiUrl); - - return ( - <> -
- - - - - - setApiUrl(e.target.value)} - placeholder="Enter API Gateway URL" fullWidth={true}/> - - - - - - - - -

- Find a concept - {/*}>*/} -

-
- - -
-
-
- - - - - -

- Browse resources -

-
- - -
-
-
- - - - ) -} diff --git a/src/frontend/app/components/AutoComplete.tsx b/src/frontend/app/components/AutoComplete.tsx deleted file mode 100644 index 5526276..0000000 --- a/src/frontend/app/components/AutoComplete.tsx +++ /dev/null @@ -1,102 +0,0 @@ -// Custom debounce function -import { - EuiBadge, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiListGroup, - EuiListGroupItem, - EuiSpacer -} from "@elastic/eui"; -import {useSearch} from "@/app/utils/search"; -import {AutoCompleteResult} from "@/app/components/AutoCompleteResult"; -import {EuiFieldText} from "@elastic/eui"; -import {EuiLoadingChart} from "@elastic/eui"; -import {EuiStat} from "@elastic/eui"; -import {EuiFormRow} from "@elastic/eui"; - - -export default function Autocomplete(props: { apiUrl: string }) { - const { - suggestions, - inputValue, - responseTime, - isLoading, - errorMessage, - handleInputChange, - handleItemClick, - handleApiUrlChange - } = useSearch(props); - - - return ( - <> - - - - - - - - - { - suggestions.length > 0 && !isLoading && !errorMessage && ( - - - - - - - - - ) - } - - - - - - {isLoading && ( - - - - )} - - - {errorMessage && ( - -

Error: {errorMessage}

-
- )} - - - {suggestions.length > 0 && !isLoading && !errorMessage && ( - <> - - - {suggestions.map((suggestion: any, index) => ( - handleItemClick(suggestion)} key={index} - style={{width: '100%'}} - label={}/> - ))} - - - - - )} - -
- - ); -} diff --git a/src/frontend/app/components/BrowseResources.tsx b/src/frontend/app/components/BrowseResources.tsx new file mode 100644 index 0000000..ee31ae0 --- /dev/null +++ b/src/frontend/app/components/BrowseResources.tsx @@ -0,0 +1,294 @@ +import React, {useEffect, useRef, useState} from 'react'; +import { + EuiBadge, + EuiBasicTable, + EuiComboBox, + EuiFieldSearch, EuiFlexGroup, EuiFlexItem, + EuiFormRow, + EuiLoadingChart, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import '@elastic/eui/dist/eui_theme_light.css'; +import ArtefactModal from './Modal'; +import {any, number} from "prop-types"; + +function prettyMilliseconds(ms: number) { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + const remainingMilliseconds = ms % 1000; + const remainingSeconds = seconds % 60; + const remainingMinutes = minutes % 60; + + let result = ""; + + if (hours) result += `${hours}h `; + if (remainingMinutes) result += `${remainingMinutes}m `; + if (remainingSeconds) result += `${remainingSeconds}s `; + if (remainingMilliseconds && ms < 1000) result += `${remainingMilliseconds}ms`; + + return result.trim(); +} + +const ArtefactsTable = (props: {apiUrl: string}) => { + const [items, setItems] = useState([]); + const [responseConfig, setResponseConfig] = useState({ + databases: Array, + totalResponseTime: number + }); + let totalResponseTime = 0 + const [loading, setLoading] = useState(true); + const [sortField, setSortField] = useState('label'); + const [sortDirection, setSortDirection] = useState("asc" as "asc" | "desc"); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedSources, setSelectedSources] = useState([]); + const [sourceOptions, setSourceOptions] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedArtefact, setSelectedArtefact] = useState(null); + const isInitialMount = useRef(true); + + const fetchArtefacts = async (apiUrl: string) => { + try { + const response = await fetch(`${apiUrl}`); + const response_json = await response.json(); + const data = response_json.collection + // @ts-ignore + const uniqueSourceNames = [...new Set(data.map(item => item.source_name))]; + + setItems(data); + setResponseConfig(response_json.responseConfig); + totalResponseTime = response_json.totalResponseTime; + + // @ts-ignore + setSourceOptions(uniqueSourceNames.map(sourceName => ({ + label: sourceName + }))) + } catch (error) { + console.error('Error fetching artefacts:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (isInitialMount.current) { + fetchArtefacts(props.apiUrl); + isInitialMount.current = false; + } + }, [props.apiUrl]); + + // Define the columns for the table + const columns: any = [ + { + + name: 'Label', + sortable: true, + render: (item: any) => ( + openModal(item)}> + {item.description} ({item.label.toString().toUpperCase()}) + + ) + }, + { + name: 'Source', + render: (item: any) => ( +
+

+ Backend Type: {item.backend_type}{' '} +

+

+ Source: {item.source_name} ({item.source}){' '} +

+
+ ), + } + ]; + + const onSourceChange = (selectedOptions: any) => { + setSelectedSources(selectedOptions.map((option: any) => option.label)); + }; + + + // Handle table sorting and pagination changes + const onTableChange = ({page = {}, sort = {}}) => { + // @ts-ignore + const {index: newPageIndex, size: newPageSize} = page; + // @ts-ignore + const {field: newSortField, direction: newSortDirection} = sort; + + setPageIndex(newPageIndex); + setPageSize(newPageSize); + setSortField(newSortField); + setSortDirection(newSortDirection); + + // Sort and paginate the items + const sortedItems = sortItems(items, newSortField, newSortDirection); + setItems(sortedItems); + }; + + // Sort items based on the selected field and direction + const sortItems = (items: never[], field: string | number, direction: string) => { + return [...items].sort((a, b) => { + const aValue = a[field]; + const bValue = b[field]; + let result = 0; + + if (aValue < bValue) { + result = -1; + } else if (aValue > bValue) { + result = 1; + } + + return direction === 'asc' ? result : -result; + }); + }; + + const filteredItems = items.filter((item: any) => { + const matchesLabelOrSourceName = item.label.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description?.toString().toLowerCase().includes(searchQuery.toLowerCase()); + + // @ts-ignore + const matchesSource = selectedSources.length === 0 || selectedSources.includes(item.source_name?.toString().toLowerCase()); + + return matchesLabelOrSourceName && matchesSource; + }); + + let groupedBySourceName = {} + + const openModal = (artefact: any) => { + setSelectedArtefact(artefact); + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + setSelectedArtefact(null); + }; + + groupedBySourceName = filteredItems.reduce((acc, item) => { + // @ts-ignore + let count = acc[item.source_name]?.count + count = (count || 0) + 1; + + + // @ts-ignore + let time = responseConfig.databases.filter((x: any) => x.url.includes(item.source))[0]?.responseTime + // @ts-ignore + acc[item.source_name] = {count: count, time: time}; + + return acc; + }, {}); + + + // Pagination logic (only show items for the current page) + const paginatedItems = filteredItems.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); + + // Custom spinner container style + const spinnerContainerStyle: any = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', + height: '200px', // Height of the container where the spinner will show + }; + + + const pagination = { + pageIndex: pageIndex, + pageSize: pageSize, + totalItemCount: filteredItems.length, + pageSizeOptions: [5, 10, 20], + }; + + // @ts-ignore + // @ts-ignore + return ( + <> + {loading ? ( +
+ + Loading resources +
+ ) : ( + <> + + + + + + + + setSearchQuery(e.target.value)} + isClearable + fullWidth + /> + + + + ({label: source}))} + onChange={onSourceChange} + isClearable + fullWidth + /> + +
+ +

Number of + Results: {filteredItems.length} ({prettyMilliseconds(totalResponseTime)})

+
+ + +
    + {Object.entries(groupedBySourceName).map(([sourceName, count]: [any, any]) => ( +
  • + {sourceName}: {count.count} {count.count > 1 ? 'results' : 'result'} ({prettyMilliseconds(count.time)}) +
  • + ))} +
+
+ + {} + + +
+
+ + + + + {isModalOpen && ( + + )} + + )} + + + + ); +}; + +export default ArtefactsTable; diff --git a/src/frontend/app/components/Modal.tsx b/src/frontend/app/components/Modal.tsx new file mode 100644 index 0000000..1e957be --- /dev/null +++ b/src/frontend/app/components/Modal.tsx @@ -0,0 +1,44 @@ +import React, {useState} from 'react'; +import { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiCodeBlock, + EuiSpacer, + useGeneratedHtmlId, +} from '@elastic/eui'; + +// @ts-ignore +export default function ArtefactModal({artefact, onClose}) { + return ( + <> + + + + {artefact.label} + + + +

Backend Type: {artefact.backend_type}

+

Source: {artefact.source}

+

Source Name: {artefact.source_name}

+

Source URL: {artefact.source_url}

+
+ + window.open(artefact.source_url, '_blank', 'noopener,noreferrer')}> + Go to {artefact.source_name} + + + + Close + + +
+ + ); +}; \ No newline at end of file diff --git a/src/frontend/app/globals.css b/src/frontend/app/globals.css index a77f31e..c004e52 100644 --- a/src/frontend/app/globals.css +++ b/src/frontend/app/globals.css @@ -1,3 +1,7 @@ .main-panel{ width: 70%; -} \ No newline at end of file +} +.euiListGroupItem__label{ + display: block; + width: 100%; +} diff --git a/src/frontend/app/page.tsx b/src/frontend/app/page.tsx index 7e9e041..3cc29b8 100644 --- a/src/frontend/app/page.tsx +++ b/src/frontend/app/page.tsx @@ -3,14 +3,14 @@ import { EuiProvider } from "@elastic/eui"; import { QueryClient, QueryClientProvider } from "react-query"; import "@elastic/eui/dist/eui_theme_light.css"; -import { MainPage } from '@/app/MainPage' +import { HomePage } from '@/app/HomePage' export default function Home() { const queryClient = new QueryClient(); return ( - + ) diff --git a/src/frontend/app/utils/resources.ts b/src/frontend/app/utils/resources.ts new file mode 100644 index 0000000..aea2f96 --- /dev/null +++ b/src/frontend/app/utils/resources.ts @@ -0,0 +1,73 @@ +import {SetStateAction, useCallback, useRef, useState} from "react"; + + +function debounce(this: any, func: { (query: any): void; apply?: any; }, wait: number) { + let timeout: string | number | NodeJS.Timeout | undefined; + return (...args: any) => { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} + +export function useResources(props: { apiUrl: string }) { + const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setError] = useState(null); + const [suggestions, setSuggestions] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [responseTime, setResponseTime] = useState(""); + const latestRequestRef = useRef(0); // Ref to track the latest request + + const fetchResources = async (query: string | any[], requestId: number, apiUrl: string) => { + if (query.length < 2) return setSuggestions([]) ; + + // Set loading state to true + setIsLoading(true); + setError(null); // Clear any previous errors + + const startTime = performance.now(); // Start timing + + try { + const response = await fetch(`${apiUrl}${query}`); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + const endTime = performance.now(); // End timing + + if (requestId === latestRequestRef.current) { + setResponseTime((endTime - startTime).toFixed(2)); // Calculate response time + setSuggestions(data ? data : []); + } + } catch (error: any) { + console.error("Error fetching suggestions:", error); + setError(error.message); // Set error message + } finally { + // Set loading state to false + setIsLoading(false); + } + }; + + // Debounced version of fetchSuggestions + const debouncedFetchSuggestions = useCallback(debounce((query) => { + const requestId = ++latestRequestRef.current; // Increment request ID + fetchResources(query, requestId, props.apiUrl).then(r => r); + }, 100), [props.apiUrl]); + + const handleInputChange = (event: { target: { value: any; }; }) => { + const value = event.target.value; + setInputValue(value); + debouncedFetchSuggestions(value); + }; + + const handleItemClick = (item: any) => { + const url = item.html_url; // Assuming each suggestion has an 'html_url' property + if (url) { + window.open(url, '_blank'); // Open URL in a new tab + } + }; + + const handleApiUrlChange = (event: { target: { value: SetStateAction; }; }) => { + setSuggestions([]); + }; + return {suggestions, inputValue, responseTime, isLoading, errorMessage, handleInputChange, handleItemClick, handleApiUrlChange}; +} diff --git a/src/frontend/app/utils/search.ts b/src/frontend/app/utils/search.ts new file mode 100644 index 0000000..69fae05 --- /dev/null +++ b/src/frontend/app/utils/search.ts @@ -0,0 +1,68 @@ +import {SetStateAction, useCallback, useRef, useState} from "react"; + + +function debounce(this: any, func: { (query: any): void; apply?: any; }, wait: number) { + let timeout: string | number | NodeJS.Timeout | undefined; + return (...args: any) => { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} + +export function useSearch(props: { apiUrl: string }) { + const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setError] = useState(null); + const [suggestions, setSuggestions] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [totalResults, setTotalResults] = useState(0) + const [responseTime, setResponseTime] = useState(""); + const latestRequestRef = useRef(0); // Ref to track the latest request + const fetchSuggestions = async (query: string | any[], requestId: number, apiUrl: string, pageSize = 20) => { + if (query.length < 2) return setSuggestions([]); + + // Set loading state to true + setIsLoading(true); + setError(null); // Clear any previous errors + + const startTime = performance.now(); // Start timing + + try { + const response = await fetch(`${apiUrl}${query}`); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + const endTime = performance.now(); // End timing + + if (requestId === latestRequestRef.current) { + setResponseTime(((endTime - startTime) / 1000).toFixed(2)); // Calculate response time + setTotalResults(data ? data.length : 0) + setSuggestions(data ? data.slice(0, pageSize) : []); + } + } catch (error: any) { + console.error("Error fetching suggestions:", error); + setError(error.message); // Set error message + } finally { + // Set loading state to false + setIsLoading(false); + } + }; + + // Debounced version of fetchSuggestions + const debouncedFetchSuggestions = useCallback(debounce((query) => { + const requestId = ++latestRequestRef.current; // Increment request ID + fetchSuggestions(query, requestId, props.apiUrl).then(r => r); + }, 100), [props.apiUrl]); + + const handleInputChange = (event: { target: { value: any; }; }) => { + const value = event.target.value; + setInputValue(value); + debouncedFetchSuggestions(value); + }; + + + const handleApiUrlChange = (event: { target: { value: SetStateAction; }; }) => { + setSuggestions([]); + }; + return {suggestions,totalResults, inputValue, responseTime, isLoading, errorMessage, handleInputChange, handleApiUrlChange}; +} diff --git a/src/frontend/next.config.js b/src/frontend/next.config.js index 6bab0b5..afcf656 100644 --- a/src/frontend/next.config.js +++ b/src/frontend/next.config.js @@ -3,7 +3,6 @@ module.exports = { output: 'export', basePath: '/api-gateway', - assetPrefix: '/api-gateway', env: { API_GATEWAY_URL: process.env.API_GATEWAY_URL || 'https://ts4nfdi-api-gateway.prod.km.k8s.zbmed.de' } diff --git a/src/frontend/public/.nojekyll b/src/frontend/public/.nojekyll new file mode 100644 index 0000000..e69de29