From 58ef09f24c86e3e15821eeaa5c4afa7340627dd7 Mon Sep 17 00:00:00 2001
From: Audrey Hamelers <hamelers@ebi.ac.uk>
Date: Mon, 28 Jun 2021 16:46:21 +0100
Subject: [PATCH] #1203

---
 app/components/activity/operations.js         |   4 +-
 app/components/manage-site/JobLogPage.jsx     | 128 ++++++++++++++++++
 .../manage-site/ManagementConsole.jsx         |   6 +-
 app/components/manage-site/index.js           |   1 +
 app/components/manage-site/operations.js      |  12 ++
 app/routes.jsx                                |   2 +
 .../xpub-model/entities/audit/data-access.js  |  43 +++++-
 server/xpub-model/entities/audit/index.js     |   3 +-
 .../entities/manuscript/data-access.js        |   2 +-
 .../xpub-server/entities/audit/resolvers.js   |   8 +-
 .../entities/audit/typeDefs.graphqls          |  13 +-
 11 files changed, 213 insertions(+), 9 deletions(-)
 create mode 100644 app/components/manage-site/JobLogPage.jsx

diff --git a/app/components/activity/operations.js b/app/components/activity/operations.js
index 161de846a..2e185f985 100644
--- a/app/components/activity/operations.js
+++ b/app/components/activity/operations.js
@@ -81,8 +81,8 @@ export const EXCEPTION_ALERT = gql`
 `
 
 export const QUERY_ACTIVITY_INFO = gql`
-  query QueryActivitiesByManuscriptId($id: ID!) {
-    activities: epmc_queryActivitiesByManuscriptId(id: $id) {
+  query ManuscriptActivityLog($id: ID!) {
+    activities: manuscriptActivityLog(id: $id) {
       ...ManuscriptFragment
       deleted
       ebiState
diff --git a/app/components/manage-site/JobLogPage.jsx b/app/components/manage-site/JobLogPage.jsx
new file mode 100644
index 000000000..5c845ecf2
--- /dev/null
+++ b/app/components/manage-site/JobLogPage.jsx
@@ -0,0 +1,128 @@
+import React from 'react'
+import { Query } from 'react-apollo'
+import moment from 'moment'
+import styled from 'styled-components'
+import { th } from '@pubsweet/ui-toolkit'
+import { JOB_LOG } from './operations'
+import { Loading, LoadingIcon, Table, Notification } from '../ui'
+import ManagementBase from './ManagementBase'
+
+const DetailsTable = styled(Table)`
+  width: 100%;
+  @media screen and (max-width: 600px) {
+    th {
+      display: none;
+    }
+    tr {
+      border: ${th('borderWidth')} ${th('borderStyle')} ${th('colorBorder')};
+    }
+  }
+`
+const TD = styled.td`
+  font-size: ${th('fontSizeBaseSmall')};
+  word-break: break-word;
+  @media screen and (max-width: 600px) {
+    display: inline-block;
+    width: 100%;
+    border: 0 !important;
+  }
+`
+const TdDate = styled(TD)`
+  vertical-align: top;
+  width: 1%;
+  white-space: nowrap;
+  @media screen and (max-width: 600px) {
+    width: 50%;
+    white-space: normal;
+  }
+`
+
+const ParseUpdate = ({ col, val }) => {
+  switch (val) {
+    case null:
+    case '':
+      return `Removed previous ${col}.`
+    case 't':
+      return `Set job ${col}`
+    case 'f':
+      return `Set job not ${col}`
+    default:
+      if (moment(val).isValid())
+        return `Job ${col} at: ${moment
+          .utc(val)
+          .local()
+          .format('DD/MM/YYYY HH:mm:ss')}`
+      return `Job ${col} is: ${val}`
+  }
+}
+
+const EventList = ({ entry: { id, originalData, changes } }) => {
+  let rowData = changes
+  if (!changes) {
+    rowData = originalData
+    Object.keys(rowData).forEach(k => rowData[k] === null && delete rowData[k])
+  }
+  const updateList = Object.keys(rowData)
+  return updateList.map(key => (
+    <React.Fragment key={id + key}>
+      <ParseUpdate col={key.replace(/_/g, ' ')} val={rowData[key]} />
+      <br />
+    </React.Fragment>
+  ))
+}
+
+const JobLog = ({ match, ...props }) => (
+  <Query
+    fetchPolicy="no-cache"
+    query={JOB_LOG}
+    variables={{ name: match.params.name }}
+  >
+    {({ data, loading }) => {
+      if (loading) {
+        return (
+          <Loading>
+            <LoadingIcon />
+          </Loading>
+        )
+      }
+      if (!data) {
+        return (
+          <Notification type="error">
+            Error loading job log from database
+          </Notification>
+        )
+      }
+      const { jobLog } = data
+      return (
+        <DetailsTable>
+          <tbody>
+            <tr>
+              <th>Date</th>
+              <th>Datacenter</th>
+              <th>Event</th>
+            </tr>
+            {jobLog.map(entry => (
+              <tr key={entry.id}>
+                <TdDate>
+                  {moment(entry.created).format('DD/MM/YYYY HH:mm')}
+                </TdDate>
+                <TdDate>{entry.dataCenter}</TdDate>
+                <TD>
+                  <EventList entry={entry} />
+                </TD>
+              </tr>
+            ))}
+          </tbody>
+        </DetailsTable>
+      )
+    }}
+  </Query>
+)
+
+const JobLogTitle = ManagementBase(JobLog)
+
+const JobLogPage = ({ ...props }) => (
+  <JobLogTitle pageTitle={`Job log: ${props.match.params.name}`} {...props} />
+)
+
+export default JobLogPage
diff --git a/app/components/manage-site/ManagementConsole.jsx b/app/components/manage-site/ManagementConsole.jsx
index 9e99d92e0..b347683cb 100644
--- a/app/components/manage-site/ManagementConsole.jsx
+++ b/app/components/manage-site/ManagementConsole.jsx
@@ -3,7 +3,7 @@ import moment from 'moment'
 import { Query } from 'react-apollo'
 import styled, { withTheme } from 'styled-components'
 import { th } from '@pubsweet/ui-toolkit'
-import { Icon, H2 } from '@pubsweet/ui'
+import { Icon, H2, Link } from '@pubsweet/ui'
 import { B, Loading, LoadingIcon, Table, Notification } from '../ui'
 import ManagementBase from './ManagementBase'
 import ConfigurationForm from './ConfigurationForm'
@@ -67,7 +67,9 @@ const Console = () => (
                   <td>
                     {' '}
                     <Desc>
-                      <B>{job.name} </B>
+                      <B>
+                        <Link to={`/manage/job/${job.name}`}>{job.name}</Link>{' '}
+                      </B>
                       <span>{job.description}</span>
                     </Desc>
                   </td>
diff --git a/app/components/manage-site/index.js b/app/components/manage-site/index.js
index f3fe73df0..b4e42d4bf 100644
--- a/app/components/manage-site/index.js
+++ b/app/components/manage-site/index.js
@@ -1,3 +1,4 @@
 export { default as UserSearch } from './UserSearch'
 export { default as ManagementConsole } from './ManagementConsole'
 export { default as MetricsPage } from './MetricsPage'
+export { default as JobLogPage } from './JobLogPage'
diff --git a/app/components/manage-site/operations.js b/app/components/manage-site/operations.js
index 4b89573ef..d05fbe98e 100644
--- a/app/components/manage-site/operations.js
+++ b/app/components/manage-site/operations.js
@@ -13,6 +13,18 @@ export const JOB_LIST = gql`
   }
 `
 
+export const JOB_LOG = gql`
+  query JobLog($name: String!) {
+    jobLog(name: $name) {
+      id
+      created
+      dataCenter
+      originalData
+      changes
+    }
+  }
+`
+
 export const PROPS = gql`
   query ListProps {
     getProps {
diff --git a/app/routes.jsx b/app/routes.jsx
index fc1883ee9..9b690bc3d 100755
--- a/app/routes.jsx
+++ b/app/routes.jsx
@@ -10,6 +10,7 @@ import {
   ManagementConsole,
   UserSearch,
   MetricsPage,
+  JobLogPage,
 } from './components/manage-site'
 import ManageAccount from './components/manage-account/ManageAccount'
 import PasswordResetEmail from './components/password-reset/PasswordResetEmailContainer'
@@ -56,6 +57,7 @@ const Routes = () => (
       <PrivateRoute component={ManagementConsole} exact path="/manage" />
       <PrivateRoute component={MetricsPage} exact path="/manage/metrics" />
       <PrivateRoute component={UserSearch} exact path="/manage/users" />
+      <PrivateRoute component={JobLogPage} exact path="/manage/job/:name" />
       <PrivateRoute
         component={ManageAccount}
         exact
diff --git a/server/xpub-model/entities/audit/data-access.js b/server/xpub-model/entities/audit/data-access.js
index daa1d1e49..b50c4b347 100644
--- a/server/xpub-model/entities/audit/data-access.js
+++ b/server/xpub-model/entities/audit/data-access.js
@@ -240,4 +240,45 @@ order by top.wk desc
   }
 }
 
-module.exports = Audit
+class JobAudit extends EpmcBaseModel {
+  static get tableName() {
+    return 'audit.job_log'
+  }
+
+  static get schema() {
+    return {
+      properties: {
+        id: { type: 'uuid' },
+        created: { type: 'timestamp' },
+        jobName: { type: 'string' },
+        dataCenter: { type: 'string' },
+        action: { type: 'string' },
+        originalData: { type: 'object' },
+        changes: { type: 'object' },
+      },
+    }
+  }
+
+  static get relationMappings() {
+    const Job = require('../config/data-access')
+    return {
+      job: {
+        relation: Model.HasOneRelation,
+        modelClass: Job,
+        join: {
+          from: 'audit.job_log.job_name',
+          to: 'config.job.name',
+        },
+      },
+    }
+  }
+
+  static async jobLog(name) {
+    return JobAudit.query()
+      .where('jobName', name)
+      .orderBy('created', 'desc')
+      .limit(100)
+  }
+}
+
+module.exports = { Audit, JobAudit }
diff --git a/server/xpub-model/entities/audit/index.js b/server/xpub-model/entities/audit/index.js
index 410471bf1..338bcc153 100644
--- a/server/xpub-model/entities/audit/index.js
+++ b/server/xpub-model/entities/audit/index.js
@@ -1,4 +1,4 @@
-const Audit = require('./data-access')
+const { Audit, JobAudit } = require('./data-access')
 
 const AuditManager = {
   modelName: 'Audit',
@@ -7,6 +7,7 @@ const AuditManager = {
   getMetrics: Audit.getMetrics,
   weeklyMetrics: Audit.weeklyMetrics,
   publisherMetrics: Audit.publisherMetrics,
+  jobLog: JobAudit.jobLog,
 }
 
 module.exports = AuditManager
diff --git a/server/xpub-model/entities/manuscript/data-access.js b/server/xpub-model/entities/manuscript/data-access.js
index e2717c2ec..2fd81ef9a 100644
--- a/server/xpub-model/entities/manuscript/data-access.js
+++ b/server/xpub-model/entities/manuscript/data-access.js
@@ -159,7 +159,7 @@ class Manuscript extends EpmcBaseModel {
     const Review = require('../review/data-access')
     const Team = require('../team/data-access')
     const User = require('../user/data-access')
-    const Audit = require('../audit/data-access')
+    const { Audit } = require('../audit/data-access')
     return {
       reviews: {
         relation: Model.HasManyRelation,
diff --git a/server/xpub-server/entities/audit/resolvers.js b/server/xpub-server/entities/audit/resolvers.js
index 10bfa147b..a541e3130 100644
--- a/server/xpub-server/entities/audit/resolvers.js
+++ b/server/xpub-server/entities/audit/resolvers.js
@@ -6,7 +6,7 @@ const { ManuscriptManager, AuditManager, OrganizationManager } = rfr(
 
 const resolvers = {
   Query: {
-    async epmc_queryActivitiesByManuscriptId(_, { id }, { user }) {
+    async manuscriptActivityLog(_, { id }, { user }) {
       if (!user) {
         throw new Error('You are not authenticated!')
       }
@@ -35,6 +35,12 @@ const resolvers = {
       const orgId = await OrganizationManager.getOrganizationID(preprint)
       return AuditManager.weeklyMetrics(orgId)
     },
+    async jobLog(_, { name }, { user }) {
+      if (!user) {
+        throw new Error('You are not authenticated!')
+      }
+      return AuditManager.jobLog(name)
+    },
   },
 }
 
diff --git a/server/xpub-server/entities/audit/typeDefs.graphqls b/server/xpub-server/entities/audit/typeDefs.graphqls
index 64e41b75d..b2da30b16 100644
--- a/server/xpub-server/entities/audit/typeDefs.graphqls
+++ b/server/xpub-server/entities/audit/typeDefs.graphqls
@@ -18,10 +18,21 @@ type Audit implements Object {
 }
 
 extend type Query {
-  epmc_queryActivitiesByManuscriptId(id: ID!): Manuscript
+  manuscriptActivityLog(id: ID!): Manuscript
   getMetrics(startMonth: Int, endMonth: Int, preprint: Boolean): [Metrics]
   weeklyMetrics(preprint: Boolean): [Metrics]
   publisherMetrics(startMonth: Int, endMonth: Int): [Stats]
+  jobLog(name: String!): [JobAudit]
+}
+
+type JobAudit {
+  id: ID!
+  created: DateTime!
+  action: String
+  dataCenter: String
+  originalData: JSON
+  changes: JSON
+  jobName: String
 }
 
 type Metrics {
-- 
GitLab