diff --git a/manager/components/JobsExplorer/JobsExplorer.js b/manager/components/JobsExplorer/JobsExplorer.js index 2952a96..000a9b6 100644 --- a/manager/components/JobsExplorer/JobsExplorer.js +++ b/manager/components/JobsExplorer/JobsExplorer.js @@ -1,5 +1,3 @@ -'use client'; - /* * */ import styles from './JobsExplorer.module.css'; @@ -11,35 +9,12 @@ import AppVersion from '@/components/AppVersion/AppVersion'; /* * */ export default function JobsExplorer() { - const handleClick = async () => { - try { - await fetch('https://printer.carrismetropolitana.pt/publish', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - owner_name: 'Teste', - owner_email: 'johnyvasconcelos@icloud.com', - owner_lang: 'pt', - gdpr_consent: true, - render_host: 'escolas.carrismetropolitana.pt', - render_path: '803239/render', - filename: 'Teste Nome PDF', - }), - }); - } catch (error) { - console.log(error); - } - }; - return (
-
CLICK
); diff --git a/manager/components/JobsExplorerTable/JobsExplorerTable.js b/manager/components/JobsExplorerTable/JobsExplorerTable.js index 3a6d00f..5591c06 100644 --- a/manager/components/JobsExplorerTable/JobsExplorerTable.js +++ b/manager/components/JobsExplorerTable/JobsExplorerTable.js @@ -9,6 +9,7 @@ import { useJobsExplorerContext } from 'contexts/JobsExplorerContext'; import JobsExplorerTableHeader from '@/components/JobsExplorerTableHeader/JobsExplorerTableHeader'; import JobsExplorerTableRowItem from '@/components/JobsExplorerTableRowItem/JobsExplorerTableRowItem'; import NoDataLabel from '@/components/NoDataLabel/NoDataLabel'; +import useSearch from 'hooks/useSearch'; /* * */ @@ -28,19 +29,38 @@ export default function JobsExplorerTable() { // // C. Transform data - const allJobsFiltered = useMemo(() => { + const allJobsSorted = useMemo(() => { if (!allJobsData) return []; - if (!jobsExplorerContext.state.status) return allJobsData; - return allJobsData.filter((item) => item.status === jobsExplorerContext.state.status); - }, [allJobsData, jobsExplorerContext.state.status]); + if (!jobsExplorerContext.state.sort_key) return allJobsData; + const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }); + if (jobsExplorerContext.state.sort_direction) return allJobsData.sort((a, b) => collator.compare(a[jobsExplorerContext.state.sort_key], b[jobsExplorerContext.state.sort_key])); + else return allJobsData.sort((a, b) => collator.compare(b[jobsExplorerContext.state.sort_key], a[jobsExplorerContext.state.sort_key])); + }, [allJobsData, jobsExplorerContext.state.sort_key, jobsExplorerContext.state.sort_direction]); + + const allJobsFilteredForStatus = useMemo(() => { + if (!allJobsSorted) return []; + if (!jobsExplorerContext.state.status) return allJobsSorted; + return allJobsSorted.filter((item) => item.status === jobsExplorerContext.state.status); + }, [allJobsSorted, jobsExplorerContext.state.status]); + + const allJobsFilteredForRenderHost = useMemo(() => { + if (!allJobsFilteredForStatus) return []; + if (!jobsExplorerContext.state.render_host) return allJobsFilteredForStatus; + return allJobsFilteredForStatus.filter((item) => item.render_host === jobsExplorerContext.state.render_host); + }, [allJobsFilteredForStatus, jobsExplorerContext.state.render_host]); + + // + // D. Handle search + + const allJobsFilteredForSearch = useSearch(jobsExplorerContext.state.search_query, allJobsFilteredForRenderHost, { keys: ['owner_name', 'owner_email', 'render_path', 'filename'] }); // - // D. Render components + // E. Render components return (
-
{allJobsFiltered?.length > 0 ? allJobsFiltered.map((jobData) => ) : }
+
{allJobsFilteredForSearch?.length > 0 ? allJobsFilteredForSearch.map((jobData) => ) : }
); diff --git a/manager/components/JobsExplorerTableHeader/JobsExplorerTableHeader.js b/manager/components/JobsExplorerTableHeader/JobsExplorerTableHeader.js index 98ab269..bad2195 100644 --- a/manager/components/JobsExplorerTableHeader/JobsExplorerTableHeader.js +++ b/manager/components/JobsExplorerTableHeader/JobsExplorerTableHeader.js @@ -1,11 +1,29 @@ /* * */ import styles from './JobsExplorerTableHeader.module.css'; +import { useJobsExplorerContext } from 'contexts/JobsExplorerContext'; import JobsExplorerTableRowContainer from '@/components/JobsExplorerTableRowContainer/JobsExplorerTableRowContainer'; /* * */ export default function JobsExplorerTableHeader() { + // + + // + // A. Setup variables + + const jobsExplorerContext = useJobsExplorerContext(); + + // + // B. Handle actions + + const handleClickColumnHeader = async (columnKey) => { + jobsExplorerContext.setSortKey(columnKey); + }; + + // + // B. Render components + return (
@@ -13,30 +31,64 @@ export default function JobsExplorerTableHeader() {
job_id
-
status
+
handleClickColumnHeader('status')}> + status +
-
notification_count
-
download_count
+
handleClickColumnHeader('notification_count')}> + notification_count +
+
handleClickColumnHeader('download_count')}> + download_count +
-
date_registered
-
date_updated
-
date_processing
-
date_ready
+
handleClickColumnHeader('date_registered')}> + date_registered +
+
handleClickColumnHeader('date_updated')}> + date_updated +
+
handleClickColumnHeader('date_processing')}> + date_processing +
+
handleClickColumnHeader('date_ready')}> + date_ready +
date_notified
date_downloaded
-
date_expired
+
handleClickColumnHeader('date_expired')}> + date_expired +
-
owner_name
-
owner_email
-
owner_lang
-
gdpr_consent
+
handleClickColumnHeader('owner_name')}> + owner_name +
+
handleClickColumnHeader('owner_email')}> + owner_email +
+
handleClickColumnHeader('owner_lang')}> + owner_lang +
+
handleClickColumnHeader('gdpr_consent')}> + gdpr_consent +
-
render_host
-
render_path
-
render_format
+
handleClickColumnHeader('render_host')}> + render_host +
+
handleClickColumnHeader('render_path')}> + render_path +
+
handleClickColumnHeader('render_format')}> + render_format +
-
filename
+
handleClickColumnHeader('filename')}> + filename +
); + + // } diff --git a/manager/components/JobsExplorerTableHeader/JobsExplorerTableHeader.module.css b/manager/components/JobsExplorerTableHeader/JobsExplorerTableHeader.module.css index 79566e7..633a20d 100644 --- a/manager/components/JobsExplorerTableHeader/JobsExplorerTableHeader.module.css +++ b/manager/components/JobsExplorerTableHeader/JobsExplorerTableHeader.module.css @@ -1,3 +1,6 @@ +/* * */ +/* CONTAINER */ + .container { display: flex; width: 100%; @@ -8,7 +11,18 @@ color: var(--reference-1); } +/* * */ +/* COLUMN */ + .column { padding: 10px; background-color: var(--color-gray-1); } + +.sortable { + cursor: pointer; +} + +.sortable:hover { + background-color: var(--color-gray-2); +} diff --git a/manager/components/JobsExplorerTableRowItem/JobsExplorerTableRowItem.js b/manager/components/JobsExplorerTableRowItem/JobsExplorerTableRowItem.js index af1937a..bac020f 100644 --- a/manager/components/JobsExplorerTableRowItem/JobsExplorerTableRowItem.js +++ b/manager/components/JobsExplorerTableRowItem/JobsExplorerTableRowItem.js @@ -43,7 +43,7 @@ export default function JobsExplorerTableRowItem({ jobData }) {
{jobData.gdpr_consent || '(empty)'}
{jobData.render_host || '(empty)'}
-
{jobData.render_path || '(empty)'}
+
{jobData.render_path.substring(0, 30) || '(empty)'}
{jobData.render_format || '(empty)'}
{jobData.filename || '(empty)'}
diff --git a/manager/components/JobsExplorerToolbar/JobsExplorerToolbar.js b/manager/components/JobsExplorerToolbar/JobsExplorerToolbar.js index 4216f17..fbe66c9 100644 --- a/manager/components/JobsExplorerToolbar/JobsExplorerToolbar.js +++ b/manager/components/JobsExplorerToolbar/JobsExplorerToolbar.js @@ -5,7 +5,7 @@ import useSWR from 'swr'; import styles from './JobsExplorerToolbar.module.css'; import { useMemo } from 'react'; -import { Select } from '@mantine/core'; +import { Select, TextInput } from '@mantine/core'; import { useJobsExplorerContext } from 'contexts/JobsExplorerContext'; import JobsExplorerCreate from '@/components/JobsExplorerCreate/JobsExplorerCreate'; import LogoutButton from '@/components/LogoutButton/LogoutButton'; @@ -33,6 +33,11 @@ export default function JobsExplorerToolbar() { return [...new Set(allJobsData.map((obj) => obj.status))]; }, [allJobsData]); + const availableRenderHosts = useMemo(() => { + if (!allJobsData) return []; + return [...new Set(allJobsData.map((obj) => obj.render_host))]; + }, [allJobsData]); + // // D. Handle actions @@ -40,12 +45,22 @@ export default function JobsExplorerToolbar() { jobsExplorerContext.setSelectedStatus(value); }; + const handleChangeSelectedRenderHost = (value) => { + jobsExplorerContext.setSelectedRenderHost(value); + }; + + const handleChangeSearchQuery = ({ currentTarget }) => { + jobsExplorerContext.setSearchQuery(currentTarget.value); + }; + // // E. Render components return (
+
diff --git a/manager/components/JobsExplorerToolbar/JobsExplorerToolbar.module.css b/manager/components/JobsExplorerToolbar/JobsExplorerToolbar.module.css index 954094b..59458f2 100644 --- a/manager/components/JobsExplorerToolbar/JobsExplorerToolbar.module.css +++ b/manager/components/JobsExplorerToolbar/JobsExplorerToolbar.module.css @@ -1,6 +1,6 @@ .container { display: grid; - grid-template-columns: 1fr 1fr auto; + grid-template-columns: 1fr 1fr 2fr 200px auto; gap: 20px; align-items: center; justify-content: center; diff --git a/manager/contexts/JobsExplorerContext.js b/manager/contexts/JobsExplorerContext.js index 8dc1217..3f1cddc 100644 --- a/manager/contexts/JobsExplorerContext.js +++ b/manager/contexts/JobsExplorerContext.js @@ -1,12 +1,20 @@ 'use client'; +/* * */ + import { createContext, useCallback, useContext, useMemo, useState } from 'react'; +/* * */ + // A. // SETUP INITIAL STATE const initialState = { + sort_key: null, + sort_direction: false, status: null, + render_host: null, + search_query: '', }; // B. @@ -35,19 +43,35 @@ export function JobsExplorerContextProvider({ children }) { // // B. Setup actions + const setSortKey = useCallback((newSortKey) => { + console.log(newSortKey); + setState((prev) => ({ ...prev, sort_key: newSortKey || null, sort_direction: prev.sort_key === newSortKey ? !prev.sort_direction : true })); + }, []); + const setSelectedStatus = useCallback((newStatus) => { setState((prev) => ({ ...prev, status: newStatus || null })); }, []); + const setSelectedRenderHost = useCallback((newRenderHost) => { + setState((prev) => ({ ...prev, render_host: newRenderHost || null })); + }, []); + + const setSearchQuery = useCallback((newQuery) => { + setState((prev) => ({ ...prev, search_query: newQuery || '' })); + }, []); + // // C. Setup context object const contextObject = useMemo( () => ({ state, + setSortKey, setSelectedStatus, + setSelectedRenderHost, + setSearchQuery, }), - [setSelectedStatus, state] + [state, setSortKey, setSelectedStatus, setSelectedRenderHost, setSearchQuery] ); // diff --git a/manager/hooks/useSearch.js b/manager/hooks/useSearch.js new file mode 100644 index 0000000..c58febe --- /dev/null +++ b/manager/hooks/useSearch.js @@ -0,0 +1,39 @@ +import { transliterate } from 'inflected'; + +export default function useSearch(query, data, options) { + // + + // Return all data if query is empty + if (!query) return data; + + // Normalize the query to remove non-English characters + const normalizedQuery = transliterate(query.toLowerCase()); + + // Check if 'keys' option is present + if (options && options.keys) { + return data.filter((item) => { + let hasMatch = false; + for (let key of options.keys) { + if (item.hasOwnProperty(key)) { + const value = item[key]; + const stringifiedValue = value ? String(value).toLowerCase() : ''; + const normalizedValue = transliterate(stringifiedValue); + if (normalizedValue.includes(normalizedQuery)) { + hasMatch = true; + break; + } + } + } + return hasMatch; + }); + } + + // The case where options is not defined + return data.filter((item) => { + const stringifiedData = Object.values(item).join('').toLowerCase(); + const normalizedItemValue = transliterate(stringifiedData); + return normalizedItemValue.includes(normalizedQuery); + }); + + // +} diff --git a/manager/package.json b/manager/package.json index 0e7d3e1..6e22a33 100644 --- a/manager/package.json +++ b/manager/package.json @@ -11,19 +11,20 @@ }, "dependencies": { "@auth/mongodb-adapter": "2.0.4", - "@mantine/core": "7.2.1", - "@mantine/dates": "7.2.1", - "@mantine/form": "7.2.1", - "@mantine/hooks": "7.2.1", - "@mantine/modals": "7.2.1", + "@mantine/core": "7.2.2", + "@mantine/dates": "7.2.2", + "@mantine/form": "7.2.2", + "@mantine/hooks": "7.2.2", + "@mantine/modals": "7.2.2", "@mantine/next": "6.0.21", - "@mantine/notifications": "7.2.1", + "@mantine/notifications": "7.2.2", "@tabler/icons-react": "2.40.0", + "inflected": "2.1.0", "mongodb": "6.2.0", "next": "14.0.2", "next-auth": "4.24.5", - "next-intl": "^3.0.0-rc.10", - "nodemailer": "^6.9.7", + "next-intl": "3.0.1", + "nodemailer": "6.9.7", "react": "18.2.0", "react-dom": "18.2.0", "sharp": "0.32.6", diff --git a/manager/pages/api/jobs/create.js b/manager/pages/api/jobs/create.js index 56a54ff..bd5e07d 100644 --- a/manager/pages/api/jobs/create.js +++ b/manager/pages/api/jobs/create.js @@ -46,12 +46,13 @@ export default async function handler(req, res) { try { const newDoc = await QUEUEDB.Job.insertOne({ owner_name: 'Teste', - owner_email: 'test@email.com', + owner_email: 'new@email.com', owner_lang: 'pt', gdpr_consent: true, - render_host: 'escolas.carrismetropolitana.pt', - render_path: '803239/render', - status: 'registered', + render_host: 'folhetos.carrismetropolitana.pt', + render_path: 'something_else', + status: 'ready', + date_registered: new Date().toISOString(), }); return await res.status(200).send(newDoc); } catch (err) { diff --git a/manager/pages/api/jobs/index.js b/manager/pages/api/jobs/index.js index 161c6d0..492ebbe 100644 --- a/manager/pages/api/jobs/index.js +++ b/manager/pages/api/jobs/index.js @@ -45,7 +45,9 @@ export default async function handler(req, res) { try { const allDocuments = await QUEUEDB.Job.find({}).toArray(); - return await res.status(200).send(allDocuments); + const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }); + const sortedDocuments = allDocuments.sort((a, b) => collator.compare(b.date_registered, a.date_registered)); + return await res.status(200).send(sortedDocuments); } catch (err) { console.log(err); return await res.status(500).json({ message: 'Cannot list Jobs.' });