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 (
);
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.' });