From 903b35e49c021f071870c15795c0ca1acc36fbca 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 +- .../app/components/AutoCompleteResult.tsx | 19 ++ .../app/components/BrowseResources.tsx | 294 ++++++++++++++++++ src/frontend/app/components/Modal.tsx | 44 +++ src/frontend/app/utils/resources.ts | 73 +++++ src/frontend/app/utils/search.ts | 73 +++++ src/frontend/next.config.js | 1 - src/frontend/public/.nojekyll | 0 9 files changed, 507 insertions(+), 2 deletions(-) create mode 100644 .attach_pid1264587 create mode 100644 src/frontend/app/components/AutoCompleteResult.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/app/components/AutoCompleteResult.tsx b/src/frontend/app/components/AutoCompleteResult.tsx new file mode 100644 index 0000000..ccdca55 --- /dev/null +++ b/src/frontend/app/components/AutoCompleteResult.tsx @@ -0,0 +1,19 @@ +import {EuiBadge, EuiFlexGroup, EuiFlexItem} from "@elastic/eui"; + +export function AutoCompleteResult(props: { suggestion: any }) { + + return <> + + +
{props.suggestion.label}
+
+ + + {props.suggestion.backend_type} + {props.suggestion.ontology} + {props.suggestion.short_form} + + +
+ +} \ No newline at end of file diff --git a/src/frontend/app/components/BrowseResources.tsx b/src/frontend/app/components/BrowseResources.tsx new file mode 100644 index 0000000..92623f9 --- /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(50); + 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/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..56a71eb --- /dev/null +++ b/src/frontend/app/utils/search.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 useSearch(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 fetchSuggestions = 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)/1000).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 + 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 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/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