diff --git a/services/api/src/resources/problem/sql.ts b/services/api/src/resources/problem/sql.ts index e20dcd28ef..8a50e1b59f 100644 --- a/services/api/src/resources/problem/sql.ts +++ b/services/api/src/resources/problem/sql.ts @@ -38,7 +38,7 @@ export const Sql = { let q = knex('environment_problem as p') .join('environment as e', {environment: 'e.id'}, '=', {environment: 'p.environment'}) .where('p.deleted', '=', '0000-00-00 00:00:00') - .select('p.*', {environment: 'e.id'}, { name: 'e.name', project: 'e.project', + .select('p.*', {service: 'p.lagoon_service'}, {environment: 'e.id'}, { name: 'e.name', project: 'e.project', environmentType: 'e.environment_type', openshiftProjectName: 'e.openshift_project_name'}); if (environmentType.length > 0) { diff --git a/services/ui/src/components/Dates/index.js b/services/ui/src/components/Dates/index.js index f88bfff008..64d127744a 100644 --- a/services/ui/src/components/Dates/index.js +++ b/services/ui/src/components/Dates/index.js @@ -1,9 +1,9 @@ import moment from "moment"; export const getFromNowTime = (date) => { - return moment(date).fromNow(); + return moment.utc(date).fromNow(); }; export const getCreatedDate = (date) => { - return moment.utc(date).local().format('DD MM YYYY, HH:mm:ss'); + return moment.utc(date).format('DD MM YYYY, HH:mm:ssZ'); }; \ No newline at end of file diff --git a/services/ui/src/components/Problem/ContentDisplay/DrutinyContent.js b/services/ui/src/components/Problem/ContentDisplay/DrutinyContent.js index 9f8d28eaaa..6c0dccbe60 100644 --- a/services/ui/src/components/Problem/ContentDisplay/DrutinyContent.js +++ b/services/ui/src/components/Problem/ContentDisplay/DrutinyContent.js @@ -44,7 +44,7 @@ const DrutinyDisplay = ({ problem }) => { )} {data.service && (
-
{data.service}
+
{data.service}
)} {data.created && (
diff --git a/services/ui/src/components/Problem/index.js b/services/ui/src/components/Problem/index.js index 18f7169cfa..bc34559043 100644 --- a/services/ui/src/components/Problem/index.js +++ b/services/ui/src/components/Problem/index.js @@ -11,7 +11,7 @@ const Problem = ({ problem, display }) => { severity: problem.severity, source: problem.source, created: fromNowDate, - severityScore: problem.severityScore, + service: problem.service, associatedPackage: problem.version ? `${problem.associatedPackage}:${problem.version}` : problem.associatedPackage }; diff --git a/services/ui/src/components/Problems/index.js b/services/ui/src/components/Problems/index.js index 681164c9e1..01b66666d3 100644 --- a/services/ui/src/components/Problems/index.js +++ b/services/ui/src/components/Problems/index.js @@ -1,229 +1,354 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { useQuery } from "@apollo/react-hooks"; +import getSeverityEnumQuery, {getProjectOptions, getSourceOptions} from 'components/Filters/helpers'; import { bp, color, fontSize } from 'lib/variables'; import useSortableProblemsData from './sortedItems'; -import { getFromNowTime } from "components/Dates"; import Problem from "components/Problem"; +import SelectFilter from 'components/Filters'; -const Problems = ({ problems }) => { - const { sortedItems, requestSort, getClassNamesFor } = useSortableProblemsData(problems); +const getOptionsFromProblems = (problems, key) => { + let uniqueOptions= problems && + new Set(problems.filter(p => p[key]).map(p => p[key])); - const [problemTerm, setProblemTerm] = useState(''); - const [hasFilter, setHasFilter] = React.useState(false); + return [...uniqueOptions]; +}; + +const Problems = ({problems}) => { + const { sortedItems, requestSort, getClassNamesFor } = useSortableProblemsData(problems); + const [severitySelected, setSeverity] = useState([]); + const [sourceSelected, setSource] = useState([]); + const [servicesSelected, setService] = useState([]); + + const [problemTerm, setProblemTerm] = useState(''); + const [hasFilter, setHasFilter] = useState(false); + const [problemStats, setProblemStats] = useState([]); + + const severities = getOptionsFromProblems(problems, 'severity'); + const sources = getOptionsFromProblems(problems, 'source'); + const services = getOptionsFromProblems(problems, 'service'); + + + // Handlers + const handleSort = (key) => requestSort(key); + + const handleTextFilterChange = (event) => { + setHasFilter(false); + + if (event.target.value !== null || event.target.value !== '') { + setHasFilter(true); + } + setProblemTerm(event.target.value); + }; + + const handleSourceChange = (source) => { + let values = source && source.map(s => s.value) || []; + setSource(values); + }; + + const handleSeverityChange = (severity) => { + let values = severity && severity.map(s => s.value) || []; + setSeverity(values); + }; + + const handleServiceChange = (service) => { + let values = service && service.map(s => s.value) || []; + setService(values); + }; + + // Options + const severityOptions = (severity) => { + return severity && severity.map(s => ({ value: s, label: s})); + }; + + const sourceOptions = (sources) => { + return sources && sources.map(s => ({ value: s, label: s})); + }; + + const serviceOptions = (services) => { + return services && services.map(s => ({ value: s, label: s})); + }; + + // Selector filtering + const matchesSeveritySelector = (item) => { + return (severitySelected.length > 0) ? + Object.keys(item).some(key => { + if (item[key] !== null) { + return severitySelected.indexOf(item['severity'].toString()) > -1; + }; + }) + : true; + } - const handleProblemFilterChange = (event) => { - setHasFilter(false); + const matchesSourceSelector = (item) => { + return (sourceSelected.length > 0) ? + Object.keys(item).some(key => { + if (item[key] !== null) { + return sourceSelected.indexOf(item['source'].toString()) > -1; + }; + }) + : true; + } - if (event.target.value !== null || event.target.value !== '') { - setHasFilter(true); + const matchesServiceSelector = (item) => { + return (servicesSelected.length > 0) ? + Object.keys(item).some(key => { + if (item[key] !== null) { + return servicesSelected.indexOf(item['service'].toString()) > -1; + }; + }) + : true; + } + + const matchesTextFilter = (item) => { + return (problemTerm != null || problemTerm !== '') ? + Object.keys(item).some(key => { + if (item[key] !== null) { + return item[key].toString().toLowerCase().includes(problemTerm.toLowerCase()); } - setProblemTerm(event.target.value); - }; + }) + : true; + } - const handleSort = (key) => { - return requestSort(key); + const shouldItemBeShown = (item) => { + return (matchesSeveritySelector(item) && matchesServiceSelector(item) && matchesSourceSelector(item) && matchesTextFilter(item)); + }; + + useEffect(() => { + let stats = { + 'critical': sortedItems.filter(p => p.severity === 'CRITICAL').length, + 'high': sortedItems.filter(p => p.severity === 'HIGH').length, + 'medium': sortedItems.filter(p => p.severity === 'MEDIUM').length, + 'low': sortedItems.filter(p => p.severity === 'LOW').length }; - const filterResults = (item) => { - const lowercasedFilter = problemTerm.toLowerCase(); - if (problemTerm == null || problemTerm === '') { - return problems; + if (stats != problemStats) { + setProblemStats(stats); + } + }, []); + + return ( +
+
+
    +
  • {Object.keys(sortedItems).length}
  • +
  • {problemStats.critical}
  • +
  • {problemStats.high}
  • +
  • {problemStats.medium}
  • +
  • {problemStats.low}
  • +
+
+
+
+ + + +
+
+
+ +
+
+ + + + + + +
+
+ {sortedItems.filter(item => shouldItemBeShown(item)).length == 0 && +
+
+ No Problems +
+
} + {sortedItems + .filter(item => shouldItemBeShown(item)) + .map((problem) => ) + } +
+ -
- ); + .filters-wrapper { + .select-filters { + display: flex; + flex-direction: column; + @media ${bp.wideUp} { + flex-flow: row; + } + + &:first-child { + padding-bottom: 1em; + } + } + } + + .text-large { + font-size: 1.4em; + } + + .red { + color: ${color.red}; + } + + .blue { + color: ${color.blue}; + } + + .yellow { + color: ${color.lightestBlue}; + } + + .grey { + color: ${color.grey}; + } + + input#filter { + width: 100%; + border: none; + padding: 10px 20px; + margin: 0; + } + + .button-sort { + color: #5f6f7a; + font-family: 'source-code-pro',sans-serif; + font-size: 12px; + font-size: 0.8125rem; + line-height: 1.4; + text-transform: uppercase; + text-align: center; + border: none; + background: none; + cursor: pointer; + padding: 0; + width: calc(100% / 6); + + &.identifier { + text-align: left; + } + + &.ascending:after { + content: ' \\25B2'; + } + + &.descending:after { + content: ' \\25BC'; + } + } + + .overview { + .overview-list { + display: flex; + justify-content: space-between; + padding: 10px 20px; + margin: 0 0 20px; + background: #f3f3f3; + + li.result { + display: flex; + flex-direction: column; + margin: 0; + } + } + } + + .data-none { + border: 1px solid ${color.white}; + border-bottom: 1px solid ${color.lightestGrey}; + border-radius: 3px; + line-height: 1.5rem; + padding: 8px 0 7px 0; + text-align: center; + } + `} +
+ ); }; -export default Problems; +export default Problems; \ No newline at end of file diff --git a/services/ui/src/components/Problems/sortedItems.js b/services/ui/src/components/Problems/sortedItems.js index b9ba246ca6..fa9ee4cfaf 100644 --- a/services/ui/src/components/Problems/sortedItems.js +++ b/services/ui/src/components/Problems/sortedItems.js @@ -41,10 +41,10 @@ const useSortableProblemsData = (initialItems) => { } const requestSort = (key) => { - let direction = 'ascending'; + let direction = key !== 'created' ? 'ascending' : 'descending'; - if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { - direction = 'descending'; + if (sortConfig && sortConfig.key === key && sortConfig.direction === direction) { + direction = direction === 'ascending' ? 'descending' : 'ascending'; } setCurrentItems(sortedItems);