diff --git a/client/src/api/managment-service.ts b/client/src/api/managment-service.ts index 9993048f..df4b5117 100644 --- a/client/src/api/managment-service.ts +++ b/client/src/api/managment-service.ts @@ -45,7 +45,7 @@ interface TokenInfo { export type AccuracyLevel = 'HIGH' | 'MEDIUM' | 'LOW'; export type UrgencyLevel = 'HIGH' | 'MEDIUM' | 'LOW'; -export type ReportType = 'ON-DEMAND' | 'SCHEDULED'; +export type ReportType = 'ON_DEMAND' | 'SCHEDULED'; export interface ReportAwaitingGeneration { clusterId: string; @@ -59,11 +59,11 @@ export interface ReportSummary { id: string; clusterId: string; title: string; - urgency: UrgencyLevel; + urgency: UrgencyLevel | null; requestedAtMs: number; sinceMs: number; toMs: number; - [key: string]: string | number; + [key: string]: string | number | null; } export interface ReportDetails { @@ -81,23 +81,20 @@ export interface ReportDetails { export interface ClusterSummary { clusterId: string; - running: boolean; + updatedAtMillis: number; accuracy: AccuracyLevel; - updatedAt: number; - slackChannels: { - name: string; + running: boolean; + slackReceivers: { + receiverName: string; webhookUrl: string; - updatedAt: number; }[]; - discordChannels: { - name: string; + discordReceivers: { + receiverName: string; webhookUrl: string; - updatedAt: number; }[]; - mailChannels: { - name: string; - email: string; - updatedAt: number; + emailReceivers: { + receiverName: string; + receiverEmail: string; }[]; } @@ -478,18 +475,7 @@ class ManagmentServiceApi { public async getClusters(): Promise { await this.refreshTokenIfExpired(); const response = await this.axiosInstance.get('api/v1/clusters'); - const clusters = response.data; - - return clusters.map((cluster: ClusterSummary) => { - return { - ...cluster, - updatedAt: 0, - accuracy: 'LOW', - slackChannels: [], - discordChannels: [], - mailChannels: [], - }; - }); + return response.data; } public async getNotificationChannels(): Promise { diff --git a/client/src/assets/hourglass-bottom.svg b/client/src/assets/hourglass-bottom.svg new file mode 100644 index 00000000..80776652 --- /dev/null +++ b/client/src/assets/hourglass-bottom.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/src/assets/hourglass-split.svg b/client/src/assets/hourglass-split.svg new file mode 100644 index 00000000..ee966e19 --- /dev/null +++ b/client/src/assets/hourglass-split.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/src/assets/hourglass-top.svg b/client/src/assets/hourglass-top.svg new file mode 100644 index 00000000..45d23ebd --- /dev/null +++ b/client/src/assets/hourglass-top.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/src/assets/kubernetes-node-icon.svg b/client/src/assets/kubernetes-node-icon.svg new file mode 100644 index 00000000..c0f2b8e1 --- /dev/null +++ b/client/src/assets/kubernetes-node-icon.svg @@ -0,0 +1,84 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/client/src/assets/open-icon.svg b/client/src/assets/open-icon.svg new file mode 100644 index 00000000..c82800a9 --- /dev/null +++ b/client/src/assets/open-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/components/EntriesSelector/ApplicationsEntriesSelector/ApplicationsEntriesSelector.tsx b/client/src/components/EntriesSelector/ApplicationsEntriesSelector/ApplicationsEntriesSelector.tsx index ee2b1056..ca4123dc 100644 --- a/client/src/components/EntriesSelector/ApplicationsEntriesSelector/ApplicationsEntriesSelector.tsx +++ b/client/src/components/EntriesSelector/ApplicationsEntriesSelector/ApplicationsEntriesSelector.tsx @@ -29,7 +29,7 @@ const ApplicationsEntriesSelector: React.FC = header: 'Name', columnKey: 'name', customComponent: (row: ApplicationDataRow) => ( - + {row.name} ), diff --git a/client/src/components/EntriesSelector/NodesEntriesSelector/NodesEntriesSelector.tsx b/client/src/components/EntriesSelector/NodesEntriesSelector/NodesEntriesSelector.tsx index e93cb566..1ad392a4 100644 --- a/client/src/components/EntriesSelector/NodesEntriesSelector/NodesEntriesSelector.tsx +++ b/client/src/components/EntriesSelector/NodesEntriesSelector/NodesEntriesSelector.tsx @@ -1,55 +1,65 @@ import React from 'react'; import EntriesSelector from 'components/EntriesSelector/EntriesSelector'; import LinkComponent from 'components/LinkComponent/LinkComponent.tsx'; -import { NodeDataRow } from 'pages/Report/NodesSection/NodesSection.tsx'; +import {NodeDataRow} from 'pages/Report/NodesSection/NodesSection.tsx'; +import KindTag from 'components/KindTag/KindTag.tsx'; interface NodesEntriesSelectorProps { - selectedNodes: NodeDataRow[]; - setSelectedNodes: React.Dispatch>; - nodesToExclude: NodeDataRow[]; - onAdd: () => void; - onClose: () => void; - availableNodes: NodeDataRow[]; + selectedNodes: NodeDataRow[]; + setSelectedNodes: React.Dispatch>; + nodesToExclude: NodeDataRow[]; + onAdd: () => void; + onClose: () => void; + availableNodes: NodeDataRow[]; } const NodesEntriesSelector: React.FC = ({ - selectedNodes, - setSelectedNodes, - nodesToExclude, - onAdd, - onClose, - availableNodes + selectedNodes, + setSelectedNodes, + nodesToExclude, + onAdd, + onClose, + availableNodes }) => { - const getUniqueKey = (node: NodeDataRow) => node.name; + const getUniqueKey = (node: NodeDataRow) => node.name; - const columns = [ - { - header: 'Name', - columnKey: 'name', - customComponent: (row: NodeDataRow) => ( - - {row.name} - - ), - }, - ]; - - return ( - - selectedItems={selectedNodes} - setSelectedItems={setSelectedNodes} - itemsToExclude={nodesToExclude} - onAdd={onAdd} - onClose={onClose} - columns={columns} - items={availableNodes} - getKey={getUniqueKey} - entityLabel="node" - noEntriesMessage={

There is no node to add.

} - title="Select Nodes" + const columns = [ + { + header: 'Name', + columnKey: 'name', + customComponent: (row: NodeDataRow) => ( + + {row.name} + + ), + }, + { + header: 'Kind', + columnKey: '', + customComponent: () => ( + - ); + ), + }, + ]; + + return ( + + selectedItems={selectedNodes} + setSelectedItems={setSelectedNodes} + itemsToExclude={nodesToExclude} + onAdd={onAdd} + onClose={onClose} + columns={columns} + items={availableNodes} + getKey={getUniqueKey} + entityLabel="node" + noEntriesMessage={

There is no node to add.

} + title="Select Nodes" + /> + ); }; export default NodesEntriesSelector; diff --git a/client/src/components/EntriesSelector/NotificationsEntriesSelector/NotificationsEntriesSelector.tsx b/client/src/components/EntriesSelector/NotificationsEntriesSelector/NotificationsEntriesSelector.tsx index 3e1731c2..9404a886 100644 --- a/client/src/components/EntriesSelector/NotificationsEntriesSelector/NotificationsEntriesSelector.tsx +++ b/client/src/components/EntriesSelector/NotificationsEntriesSelector/NotificationsEntriesSelector.tsx @@ -39,7 +39,7 @@ const NotificationsEntriesSelector: React.FC< { header: 'Name', columnKey: 'name', - customComponent: (row) => {row.name}, + customComponent: (row) => {row.name}, }, { header: 'Service', diff --git a/client/src/components/Hourglass/Hourglass.scss b/client/src/components/Hourglass/Hourglass.scss index e5bf4a07..df3752d7 100644 --- a/client/src/components/Hourglass/Hourglass.scss +++ b/client/src/components/Hourglass/Hourglass.scss @@ -1,23 +1,64 @@ -@keyframes flip { - 0%, 50% { - transform: rotate(0deg) scale(1); +.hourglass { + position: relative; + width: 30px; + height: 30px; + animation: spinHourglass 4s linear infinite; + + &__frame-top, + &__frame-middle, + &__frame-bottom { + position: absolute; + visibility: hidden; } - 15% { - transform: rotate(10deg) scale(1.05); + + &__frame-top { + animation: cycleTop 4s linear infinite; } - 30% { - transform: rotate(-10deg) scale(1.05); + + &__frame-middle { + animation: cycleMiddle 4s linear infinite; } - 100% { - transform: rotate(180deg) scale(1); + + &__frame-bottom { + animation: cycleBottom 4s linear infinite; } } -.hourglass { - display: flex; - flex-direction: column; - align-items: center; - animation: flip 2s infinite ease-in-out; - transform-origin: center; - transition: transform 0.5s; +@keyframes cycleTop { + 0%, 25% { + visibility: visible; + } + 25.01%, 100% { + visibility: hidden; + } +} + +@keyframes cycleMiddle { + 0%, 25% { + visibility: hidden; + } + 25.01%, 50% { + visibility: visible; + } + 50.01%, 100% { + visibility: hidden; + } +} + +@keyframes cycleBottom { + 0%, 50% { + visibility: hidden; + } + 50.01%, 100% { + visibility: visible; + } +} + +@keyframes spinHourglass { + 0%, 75% { + transform: rotate(0deg); + } + 100% { + transform: rotate(180deg); + } } diff --git a/client/src/components/Hourglass/Hourglass.tsx b/client/src/components/Hourglass/Hourglass.tsx index cd251af8..469cfb6b 100644 --- a/client/src/components/Hourglass/Hourglass.tsx +++ b/client/src/components/Hourglass/Hourglass.tsx @@ -3,9 +3,17 @@ import './Hourglass.scss'; import SVGIcon from 'components/SVGIcon/SVGIcon.tsx'; const Hourglass: React.FC = () => ( -
- +
+
+
+
+ +
+
+ +
+
); export default Hourglass; diff --git a/client/src/components/IncidentList/IncidentList.tsx b/client/src/components/IncidentList/IncidentList.tsx index 53c64e5f..a458cf9c 100644 --- a/client/src/components/IncidentList/IncidentList.tsx +++ b/client/src/components/IncidentList/IncidentList.tsx @@ -1,6 +1,6 @@ import SVGIcon from 'components/SVGIcon/SVGIcon.tsx'; import './IncidentList.scss'; -import { dateFromTimestampMs } from 'lib/date'; +import { dateTimeFromTimestampMs } from 'lib/date'; import { GenericIncident } from 'types/incident'; import { UrgencyLevel } from '@api/managment-service'; @@ -44,7 +44,7 @@ const IncidentList = ({ incidents, onClick }: IncidentListProps) => {
{incident.title}
- {dateFromTimestampMs(incident.timestamp)} + {dateTimeFromTimestampMs(incident.timestamp)}
))} diff --git a/client/src/components/KindTag/KindTag.scss b/client/src/components/KindTag/KindTag.scss index be8a74f2..3decc082 100644 --- a/client/src/components/KindTag/KindTag.scss +++ b/client/src/components/KindTag/KindTag.scss @@ -23,11 +23,4 @@ justify-content: center; padding-right: 0.5rem; } - - &--spacer { - width: 130px; - background-color: transparent; - border: none; - cursor: default; - } } \ No newline at end of file diff --git a/client/src/components/KindTag/KindTag.tsx b/client/src/components/KindTag/KindTag.tsx index b6070c99..83a49bf3 100644 --- a/client/src/components/KindTag/KindTag.tsx +++ b/client/src/components/KindTag/KindTag.tsx @@ -3,20 +3,18 @@ import kubernetesDeployLogo from 'assets/kubernetes-deploy-icon.svg'; import kubernetesDSLogo from 'assets/kubernetes-ds-icon.svg'; import kubernetesSTSLogo from 'assets/kubernetes-sts-icon.svg'; import kubernetesLogo from 'assets/kubernetes-logo-icon.svg'; +import kubernetesNode from 'assets/kubernetes-node-icon.svg'; interface KindTagProps { - name?: string; + name: string; } const KindTag = ({ name }: KindTagProps) => { - if (!name) { - return
; - } - const logoMap: Record = { Deployment: kubernetesDeployLogo, DaemonSet: kubernetesDSLogo, StatefulSet: kubernetesSTSLogo, + Node: kubernetesNode, }; const selectedLogo = logoMap[name] || kubernetesLogo; diff --git a/client/src/components/LinkComponent/LinkComponent.scss b/client/src/components/LinkComponent/LinkComponent.scss index 88dff751..cab82d74 100644 --- a/client/src/components/LinkComponent/LinkComponent.scss +++ b/client/src/components/LinkComponent/LinkComponent.scss @@ -20,7 +20,6 @@ .link-component { color: $green; cursor: pointer; - text-decoration: underline; transition: all 0.1s; &:hover { diff --git a/client/src/components/LinkComponent/LinkComponent.tsx b/client/src/components/LinkComponent/LinkComponent.tsx index b49811e3..fd1be710 100644 --- a/client/src/components/LinkComponent/LinkComponent.tsx +++ b/client/src/components/LinkComponent/LinkComponent.tsx @@ -4,7 +4,7 @@ import StateBadge from 'components/StateBadge/StateBadge'; import { useNavigate } from 'react-router-dom'; interface LinkComponentProps { - to: string; + to?: string; children: React.ReactNode; className?: string; onClick?: React.MouseEventHandler; @@ -12,7 +12,7 @@ interface LinkComponentProps { } const LinkComponent: React.FC = ({ - to, + to='', children, className = '', onClick, diff --git a/client/src/components/PageTemplate/PageTemplate.scss b/client/src/components/PageTemplate/PageTemplate.scss index 9f96c84c..73e58fa2 100644 --- a/client/src/components/PageTemplate/PageTemplate.scss +++ b/client/src/components/PageTemplate/PageTemplate.scss @@ -10,10 +10,10 @@ width: 100%; flex-grow: 1; - @media (min-width: 1500px) { + @media (min-width: 1600px) { display: flex; flex-direction: column; - width: 1300px; + width: 1380px; margin: 0 auto; } } diff --git a/client/src/components/SVGIcon/SVGIcon.scss b/client/src/components/SVGIcon/SVGIcon.scss index a130f19f..d3a91448 100644 --- a/client/src/components/SVGIcon/SVGIcon.scss +++ b/client/src/components/SVGIcon/SVGIcon.scss @@ -150,6 +150,20 @@ } } +.open-icon { + @extend .svg-icon; + mask: url('/src/assets/open-icon.svg') no-repeat center; + + .svg-icon { + transition: fill 0.3s ease; + background-color: $component-border-color; + + &:hover { + background-color: $green; + } + } +} + .notification-icon { @extend .svg-icon; background-color: $green; @@ -186,11 +200,36 @@ mask: url('/src/assets/task_done_icon.svg') no-repeat center; } -.hourglass { +.sand-clock-top { + @extend .svg-icon; + background-color: $green; + mask: url('/src/assets/hourglass-top.svg') no-repeat center; + width: 30px; + height: 30px; + + .svg-icon { + width: 30px; + height: 30px; + } +} + +.sand-clock-middle { + @extend .svg-icon; + background-color: $green; + mask: url('/src/assets/hourglass-split.svg') no-repeat center; + width: 30px; + height: 30px; + + .svg-icon { + width: 30px; + height: 30px; + } +} + +.sand-clock-bottom { @extend .svg-icon; - transform: rotate(180deg); background-color: $green; - mask: url('/src/assets/hourglass.svg') no-repeat center; + mask: url('/src/assets/hourglass-bottom.svg') no-repeat center; width: 30px; height: 30px; diff --git a/client/src/components/Spinner/Spinner.scss b/client/src/components/Spinner/Spinner.scss index cb5a1cee..053ce3dc 100644 --- a/client/src/components/Spinner/Spinner.scss +++ b/client/src/components/Spinner/Spinner.scss @@ -6,15 +6,19 @@ border-radius: 50%; width: 40px; height: 40px; - animation: spin 0.7s linear infinite; - margin: 20px auto; + animation: spin 1.2s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } - + 10% { + transform: rotate(20deg); + } + 90% { + transform: rotate(340deg); + } 100% { transform: rotate(360deg); } diff --git a/client/src/components/Spinner/Spinner.tsx b/client/src/components/Spinner/Spinner.tsx index 85a92e9e..2a1353c3 100644 --- a/client/src/components/Spinner/Spinner.tsx +++ b/client/src/components/Spinner/Spinner.tsx @@ -1,9 +1,16 @@ import React from 'react'; import './Spinner.scss'; -const Spinner: React.FC = () => { +interface SpinnerProps { + size?: string; +} + +const Spinner: React.FC = ({ size = '40px' }) => { return ( -
+
); }; diff --git a/client/src/lib/date.ts b/client/src/lib/date.ts index 38cb30ea..fc924b20 100644 --- a/client/src/lib/date.ts +++ b/client/src/lib/date.ts @@ -1,10 +1,33 @@ export const defaultDateFromUnixTimestamp = (timestamp: number): string => new Date(timestamp * 1000).toLocaleString(); -export const dateFromTimestampMs = (timestamp: number): string => { +export const dateTimeFromTimestampMs = (timestamp: number): string => { return new Date(timestamp).toLocaleString(); }; +export const dateFromTimestampMs = (timestamp: number): string => { + const date = new Date(timestamp); + + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = date.getFullYear(); + + return `${day}.${month}.${year}`; +}; + +export const dateTimeWithoutSecondsFromTimestampMs = (timestamp: number): string => { + const date = new Date(timestamp); + + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = date.getFullYear(); + + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + return `${day}.${month}.${year} ${hours}:${minutes}`; +}; + export const dateOnlyFromTimestampMs = (timestamp: number): string => { return new Date(timestamp).toLocaleDateString(); }; diff --git a/client/src/pages/Clusters/Clusters.tsx b/client/src/pages/Clusters/Clusters.tsx index a1fd0894..7855ad02 100644 --- a/client/src/pages/Clusters/Clusters.tsx +++ b/client/src/pages/Clusters/Clusters.tsx @@ -1,10 +1,10 @@ import SectionComponent from 'components/SectionComponent/SectionComponent'; import PageTemplate from 'components/PageTemplate/PageTemplate'; import HeaderWithIcon from 'components/PageTemplate/components/HeaderWithIcon/HeaderWithIcon'; -import Table, { TableColumn } from 'components/Table/Table'; +import Table, {TableColumn} from 'components/Table/Table'; import './Clusters.scss'; import Channels from './components/NotificationChannelsColumn/NotificationChannelsColumn'; -import { useEffect, useState } from 'react'; +import {useEffect, useState} from 'react'; import { ClusterSummary, ManagmentServiceApiInstance, @@ -16,6 +16,7 @@ import LinkComponent from 'components/LinkComponent/LinkComponent.tsx'; import Spinner from 'components/Spinner/Spinner.tsx'; import ReportActionsCell from './ReportActionsCell'; import AccuracyBadge from 'components/AccuracyBadge/AccuracyBadge.tsx'; +import {dateTimeFromTimestampMs} from 'lib/date.ts'; interface ClusterDataRow { name: string; @@ -23,6 +24,7 @@ interface ClusterDataRow { accuracy: AccuracyLevel; notificationChannels: NotificationChannelColumn[]; updatedAt: string; + [key: string]: string | NotificationChannelColumn[]; } @@ -34,35 +36,30 @@ export interface NotificationChannelColumn { const transformNotificationChannelsToColumns = ( cluster: ClusterSummary, ): NotificationChannelColumn[] => { - return cluster.slackChannels + return cluster.slackReceivers .map( (channel): NotificationChannelColumn => ({ kind: 'SLACK', - name: channel.name, + name: channel.receiverName, }), ) .concat( - cluster.discordChannels.map((channel) => ({ + cluster.discordReceivers.map((channel) => ({ kind: 'DISCORD', - name: channel.name, + name: channel.receiverName, })), ) .concat( - cluster.mailChannels.map((channel) => ({ + cluster.emailReceivers.map((channel) => ({ kind: 'EMAIL', - name: channel.email, + name: channel.receiverName, })), ); }; const transformIsRunningLabel = ( cluster: ClusterSummary, ): ClusterDataRow['state'] => { - return cluster.running? 'ONLINE' : 'OFFLINE'; -}; - -const transformUpdatedAtDate = (cluster: ClusterSummary) => { - const date = new Date(cluster.updatedAt); - return date.toLocaleString(); + return cluster.running ? 'ONLINE' : 'OFFLINE'; }; const columns: Array> = [ @@ -81,14 +78,17 @@ const columns: Array> = [ { header: 'Notification', columnKey: 'notificationChannels', - customComponent: ({ notificationChannels }) => ( - - ), + customComponent: ({ notificationChannels }: + { notificationChannels: NotificationChannelColumn[] }) => { + return ; + }, }, { header: 'Accuracy', columnKey: 'accuracy', - customComponent: ({ accuracy }) => , + customComponent: (row: ClusterDataRow) => { + return ; + }, }, { header: 'Updated at', @@ -97,9 +97,9 @@ const columns: Array> = [ { header: 'Reports', columnKey: 'actions', - customComponent: (row: ClusterDataRow) => ( - - ), + customComponent: (row: ClusterDataRow) => { + return ; + }, }, ]; @@ -117,7 +117,7 @@ const Clusters = () => { accuracy: cluster.accuracy, state: transformIsRunningLabel(cluster), notificationChannels: transformNotificationChannelsToColumns(cluster), - updatedAt: transformUpdatedAtDate(cluster), + updatedAt: dateTimeFromTimestampMs(cluster.updatedAtMillis), }), ); @@ -132,17 +132,17 @@ const Clusters = () => { fetchClusters(); }, []); - const header = ; + const header = ; return ( } + icon={} > - {isLoading && } + {isLoading && } {!isLoading && clusters.length > 0 && ( - +
)} {!isLoading && clusters.length === 0 && (
No registered clusters yet
diff --git a/client/src/pages/Home/Home.tsx b/client/src/pages/Home/Home.tsx index f523813f..c1966c5f 100644 --- a/client/src/pages/Home/Home.tsx +++ b/client/src/pages/Home/Home.tsx @@ -13,7 +13,7 @@ const Home = () => { const fetchReports = async () => { try { const [onDemandReports, scheduledReports] = await Promise.all([ - ManagmentServiceApiInstance.getReports('ON-DEMAND'), + ManagmentServiceApiInstance.getReports('ON_DEMAND'), ManagmentServiceApiInstance.getReports('SCHEDULED'), ]); const reports = [...onDemandReports, ...scheduledReports]; diff --git a/client/src/pages/Home/components/ReportTitle/ReportTitle.tsx b/client/src/pages/Home/components/ReportTitle/ReportTitle.tsx index 1445e260..870cd165 100644 --- a/client/src/pages/Home/components/ReportTitle/ReportTitle.tsx +++ b/client/src/pages/Home/components/ReportTitle/ReportTitle.tsx @@ -1,6 +1,6 @@ import React from 'react'; import './ReportTitle.scss'; -import { dateFromTimestampMs } from 'lib/date'; +import { dateTimeFromTimestampMs } from 'lib/date'; interface ReportTitleProps { source: string; @@ -20,7 +20,7 @@ const ReportTitle: React.FC = ({ {source}

- ({dateFromTimestampMs(startTime)} - {dateFromTimestampMs(endTime)}) + ({dateTimeFromTimestampMs(startTime)} - {dateTimeFromTimestampMs(endTime)})

); diff --git a/client/src/pages/Incident/components/ApplicationMetadataSection/ApplicationMetadataSection.tsx b/client/src/pages/Incident/components/ApplicationMetadataSection/ApplicationMetadataSection.tsx index 7cb27f03..288d10b9 100644 --- a/client/src/pages/Incident/components/ApplicationMetadataSection/ApplicationMetadataSection.tsx +++ b/client/src/pages/Incident/components/ApplicationMetadataSection/ApplicationMetadataSection.tsx @@ -2,7 +2,7 @@ import LabelField from 'components/LabelField/LabelField'; import SectionComponent from 'components/SectionComponent/SectionComponent'; import './ApplicationMetadataSection.scss'; import SVGIcon from 'components/SVGIcon/SVGIcon'; -import { dateFromTimestampMs } from 'lib/date'; +import { dateTimeFromTimestampMs } from 'lib/date'; interface ApplicationMetadataSectionParams { clusterId: string; @@ -31,11 +31,11 @@ const ApplicationMetadataSection = ({
diff --git a/client/src/pages/Incident/components/IncidentHeader/IncidentHeader.tsx b/client/src/pages/Incident/components/IncidentHeader/IncidentHeader.tsx index 10c84aba..d529dac5 100644 --- a/client/src/pages/Incident/components/IncidentHeader/IncidentHeader.tsx +++ b/client/src/pages/Incident/components/IncidentHeader/IncidentHeader.tsx @@ -1,7 +1,7 @@ import HeaderWithIcon from 'components/PageTemplate/components/HeaderWithIcon/HeaderWithIcon'; import SVGIcon from 'components/SVGIcon/SVGIcon'; import './IncidentHeader.scss'; -import { dateFromTimestampMs } from 'lib/date'; +import { dateTimeFromTimestampMs } from 'lib/date'; interface IncidentHeaderProps { id: string; @@ -14,7 +14,7 @@ const IncidentHeader = ({ name, timestamp }: IncidentHeaderProps) => {
{name}
- {dateFromTimestampMs(timestamp)} + {dateTimeFromTimestampMs(timestamp)}
); diff --git a/client/src/pages/Incident/components/NodeMetadataSection/NodeMetadataSection.tsx b/client/src/pages/Incident/components/NodeMetadataSection/NodeMetadataSection.tsx index ee22a8cf..2f215d2c 100644 --- a/client/src/pages/Incident/components/NodeMetadataSection/NodeMetadataSection.tsx +++ b/client/src/pages/Incident/components/NodeMetadataSection/NodeMetadataSection.tsx @@ -2,7 +2,7 @@ import LabelField from 'components/LabelField/LabelField'; import SectionComponent from 'components/SectionComponent/SectionComponent'; import './NodeMetadataSection.scss'; import SVGIcon from 'components/SVGIcon/SVGIcon'; -import { dateFromTimestampMs } from 'lib/date'; +import { dateTimeFromTimestampMs } from 'lib/date'; interface NodeMetadataSectionParams { nodeName: string; @@ -28,11 +28,11 @@ const NodeMetadataSection = ({
diff --git a/client/src/pages/Notification/NotificationTable/DiscordTable.tsx b/client/src/pages/Notification/NotificationTable/DiscordTable.tsx index 8e0ecd0b..f59003aa 100644 --- a/client/src/pages/Notification/NotificationTable/DiscordTable.tsx +++ b/client/src/pages/Notification/NotificationTable/DiscordTable.tsx @@ -12,7 +12,7 @@ import { } from 'api/managment-service'; import LoadingTable from './LoadingTable'; import NewDiscordChannelPopup from 'pages/Notification/NewChannelPopup/NewDiscordChannelPopup'; -import { dateFromTimestampMs } from 'lib/date'; +import { dateTimeFromTimestampMs } from 'lib/date'; import EditDiscordChannelPopup from 'pages/Notification/EditChannelPopup/EditDiscordChannelPopup'; import './NotificationTable.scss'; import { useToast } from 'providers/ToastProvider/ToastProvider'; @@ -29,8 +29,8 @@ const getDiscordChannelTableRow = ({ webhookUrl, }: DiscordNotificationChannel): DiscordTableRowProps => ({ name: receiverName, - updatedAt: dateFromTimestampMs(updatedAt), - createdAt: dateFromTimestampMs(createdAt), + updatedAt: dateTimeFromTimestampMs(updatedAt), + createdAt: dateTimeFromTimestampMs(createdAt), webhookUrl: webhookUrl, id, }); diff --git a/client/src/pages/Notification/NotificationTable/EmailTable.tsx b/client/src/pages/Notification/NotificationTable/EmailTable.tsx index e6a18f19..6bd4810f 100644 --- a/client/src/pages/Notification/NotificationTable/EmailTable.tsx +++ b/client/src/pages/Notification/NotificationTable/EmailTable.tsx @@ -12,7 +12,7 @@ import { import LoadingTable from './LoadingTable'; import EmailColumn from 'pages/Notification/EmailCell/EmailCell'; import NewEmailChannelPopup from 'pages/Notification/NewChannelPopup/NewEmailChannelPopup'; -import { dateFromTimestampMs } from 'lib/date'; +import { dateTimeFromTimestampMs } from 'lib/date'; import EditEmailChannelPopup from 'pages/Notification/EditChannelPopup/EditEmailChannelPopup'; import './NotificationTable.scss'; import { useToast } from 'providers/ToastProvider/ToastProvider'; @@ -29,8 +29,8 @@ const getEmailChannelTableRow = ({ receiverEmail, }: EmailNotificationChannel): EmailTableRowProps => ({ name: receiverName, - updatedAt: dateFromTimestampMs(updatedAt), - createdAt: dateFromTimestampMs(createdAt), + updatedAt: dateTimeFromTimestampMs(updatedAt), + createdAt: dateTimeFromTimestampMs(createdAt), email: receiverEmail, id, }); diff --git a/client/src/pages/Notification/NotificationTable/SlackTable.tsx b/client/src/pages/Notification/NotificationTable/SlackTable.tsx index 40d1afab..1f235747 100644 --- a/client/src/pages/Notification/NotificationTable/SlackTable.tsx +++ b/client/src/pages/Notification/NotificationTable/SlackTable.tsx @@ -12,7 +12,7 @@ import { } from 'api/managment-service'; import LoadingTable from './LoadingTable'; import NewSlackChannelPopup from 'pages/Notification/NewChannelPopup/NewSlackChannelPopup'; -import { dateFromTimestampMs } from 'lib/date'; +import { dateTimeFromTimestampMs } from 'lib/date'; import EditSlackChannelPopup from 'pages/Notification/EditChannelPopup/EditSlackChannelPopup'; import './NotificationTable.scss'; import { useToast } from 'providers/ToastProvider/ToastProvider'; @@ -28,8 +28,8 @@ const getSlackChannelTableRow = ({ webhookUrl, }: SlackNotificationChannel): SlackTableRowProps => ({ name: receiverName, - updatedAt: dateFromTimestampMs(updatedAt), - createdAt: dateFromTimestampMs(createdAt), + updatedAt: dateTimeFromTimestampMs(updatedAt), + createdAt: dateTimeFromTimestampMs(createdAt), webhookUrl, id, }); diff --git a/client/src/pages/Report/ApplicationSection/ApplicationSection.tsx b/client/src/pages/Report/ApplicationSection/ApplicationSection.tsx index be7eeace..44c8ce46 100644 --- a/client/src/pages/Report/ApplicationSection/ApplicationSection.tsx +++ b/client/src/pages/Report/ApplicationSection/ApplicationSection.tsx @@ -106,7 +106,7 @@ const ApplicationSection: React.FC = ({ header: 'Name', columnKey: 'name', customComponent: (app: ApplicationDataRow) => ( - + {app.name} ), diff --git a/client/src/pages/Report/CreateReport.tsx b/client/src/pages/Report/CreateReport.tsx index 829099c9..a94fad6f 100644 --- a/client/src/pages/Report/CreateReport.tsx +++ b/client/src/pages/Report/CreateReport.tsx @@ -24,7 +24,7 @@ const CreateReport = () => { const [applications, setApplications] = useState([]); const [nodes, setNodes] = useState([]); const [accuracy, setAccuracy] = useState('HIGH'); - const [generationType, setGenerationType] = useState('ON-DEMAND'); + const [generationType, setGenerationType] = useState('ON_DEMAND'); const [generationPeriod, setGenerationPeriod] = useState(schedulePeriodOptions.periods[2]); const navigate = useNavigate(); @@ -78,7 +78,7 @@ const CreateReport = () => { - {generationType === 'ON-DEMAND' ? ( + {generationType === 'ON_DEMAND' ? ( ) : ( diff --git a/client/src/pages/Report/CreateReportUtils.tsx b/client/src/pages/Report/CreateReportUtils.tsx index 65147756..61a432ce 100644 --- a/client/src/pages/Report/CreateReportUtils.tsx +++ b/client/src/pages/Report/CreateReportUtils.tsx @@ -10,7 +10,7 @@ import { ReportType, } from 'api/managment-service.ts'; import {periodToMilliseconds} from './SchedulePeriod/SchedulePeriod.tsx'; -import {dateFromTimestampMs} from 'lib/date.ts'; +import {dateTimeFromTimestampMs} from 'lib/date.ts'; export const fetchClusterData = async ( clusterId: string @@ -42,24 +42,24 @@ export const fetchClusterData = async ( name: receiver.receiverName, details: receiver.webhookUrl, service: 'SLACK' as NotificationChannelKind, - added: dateFromTimestampMs(receiver.createdAt), - updated: dateFromTimestampMs(receiver.updatedAt), + added: dateTimeFromTimestampMs(receiver.createdAt), + updated: dateTimeFromTimestampMs(receiver.updatedAt), })), ...clusterDetails.discordReceivers.map(receiver => ({ id: receiver.id.toString(), name: receiver.receiverName, details: receiver.webhookUrl, service: 'DISCORD' as NotificationChannelKind, - added: dateFromTimestampMs(receiver.createdAt), - updated: dateFromTimestampMs(receiver.updatedAt), + added: dateTimeFromTimestampMs(receiver.createdAt), + updated: dateTimeFromTimestampMs(receiver.updatedAt), })), ...clusterDetails.emailReceivers.map(receiver => ({ id: receiver.id.toString(), name: receiver.receiverName, details: receiver.receiverEmail, service: 'EMAIL' as NotificationChannelKind, - added: dateFromTimestampMs(receiver.createdAt), - updated: dateFromTimestampMs(receiver.updatedAt), + added: dateTimeFromTimestampMs(receiver.createdAt), + updated: dateTimeFromTimestampMs(receiver.updatedAt), })), ]; @@ -141,7 +141,7 @@ export const generateReport = ({ const schedulePeriodMs = periodToMilliseconds[generationPeriod] || 0; - if (generationType === 'ON-DEMAND') { + if (generationType === 'ON_DEMAND') { const report: ReportPost = { clusterId: id ?? '', accuracy: 'HIGH', diff --git a/client/src/pages/Report/NodesSection/NodesSection.tsx b/client/src/pages/Report/NodesSection/NodesSection.tsx index b3abf69f..9cce60e2 100644 --- a/client/src/pages/Report/NodesSection/NodesSection.tsx +++ b/client/src/pages/Report/NodesSection/NodesSection.tsx @@ -103,7 +103,7 @@ const NodesSection: React.FC = ({ header: 'Name', columnKey: 'name', customComponent: (node: NodeDataRow) => ( - + {node.name} ), @@ -130,10 +130,12 @@ const NodesSection: React.FC = ({ ), }, { - header: '', + header: 'Kind', columnKey: '', customComponent: () => ( - + ), }, { diff --git a/client/src/pages/Report/NotificationSection/NotificationChannelTable.tsx b/client/src/pages/Report/NotificationSection/NotificationChannelTable.tsx index e440beef..1498a888 100644 --- a/client/src/pages/Report/NotificationSection/NotificationChannelTable.tsx +++ b/client/src/pages/Report/NotificationSection/NotificationChannelTable.tsx @@ -23,7 +23,7 @@ const NotificationChannelTable: React.FC = ({ header: 'Name', columnKey: 'name', customComponent: (row) => ( - {row.name} + {row.name} ), }, { diff --git a/client/src/pages/Report/NotificationSection/NotificationSection.tsx b/client/src/pages/Report/NotificationSection/NotificationSection.tsx index 220df75d..2ed873a9 100644 --- a/client/src/pages/Report/NotificationSection/NotificationSection.tsx +++ b/client/src/pages/Report/NotificationSection/NotificationSection.tsx @@ -6,7 +6,7 @@ import OverlayComponent from 'components/OverlayComponent/OverlayComponent.tsx'; import NotificationsEntriesSelector from 'components/EntriesSelector/NotificationsEntriesSelector/NotificationsEntriesSelector.tsx'; import {ManagmentServiceApiInstance} from 'api/managment-service.ts'; -import {dateFromTimestampMs} from 'lib/date.ts'; +import {dateTimeFromTimestampMs} from 'lib/date.ts'; export interface NotificationChannel { id: string; @@ -42,8 +42,8 @@ const NotificationSection: React.FC = ({ name: channel.name, service: channel.service, details: channel.details, - updated: dateFromTimestampMs(channel.updated), - added: dateFromTimestampMs(channel.added), + updated: dateTimeFromTimestampMs(channel.updated), + added: dateTimeFromTimestampMs(channel.added), }), )) } catch (error) { diff --git a/client/src/pages/Report/SchedulePeriod/SchedulePeriod.tsx b/client/src/pages/Report/SchedulePeriod/SchedulePeriod.tsx index 9d999643..01a7564b 100644 --- a/client/src/pages/Report/SchedulePeriod/SchedulePeriod.tsx +++ b/client/src/pages/Report/SchedulePeriod/SchedulePeriod.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useState} from 'react'; import SectionComponent from 'components/SectionComponent/SectionComponent'; import SVGIcon from 'components/SVGIcon/SVGIcon'; import TagButton from 'components/TagButton/TagButton'; -import { dateFromTimestampMs } from 'lib/date'; +import { dateTimeFromTimestampMs } from 'lib/date'; import './SchedulePeriod.scss'; export interface SchedulePeriodProps { @@ -34,7 +34,7 @@ const SchedulePeriod: React.FC = ({ setGenerationPeriod }) const now = Date.now(); const periodMs = periodToMilliseconds[period] || 0; const nextTimestamp = now + periodMs; - setNextReportDate(dateFromTimestampMs(nextTimestamp)); + setNextReportDate(dateTimeFromTimestampMs(nextTimestamp)); }; calculateNextReportDate(); diff --git a/client/src/pages/Report/StateSection/ReportGenerationType.tsx b/client/src/pages/Report/StateSection/ReportGenerationType.tsx index 3be1e003..7d4d2b45 100644 --- a/client/src/pages/Report/StateSection/ReportGenerationType.tsx +++ b/client/src/pages/Report/StateSection/ReportGenerationType.tsx @@ -9,7 +9,7 @@ interface ReportGenerationTypeProps { } const ReportGenerationType: React.FC = ({ setParentGenerationType }) => { - const [generationType, setGenerationType] = useState('ON-DEMAND'); + const [generationType, setGenerationType] = useState('ON_DEMAND'); const handleGenerationTypeChange = (newGenerationType: ReportType) => { setGenerationType(newGenerationType); @@ -20,7 +20,7 @@ const ReportGenerationType: React.FC = ({ setParentGe } title={'Generation type'}>
diff --git a/client/src/pages/Reports/Reports.scss b/client/src/pages/Reports/Reports.scss index f0a96ffa..13442f88 100644 --- a/client/src/pages/Reports/Reports.scss +++ b/client/src/pages/Reports/Reports.scss @@ -1,6 +1,41 @@ +@import '@/variables'; + .reports { + display: flex; + flex-direction: column; + align-items: left; + gap: 2rem; + + &__title-with-icon { display: flex; - flex-direction: column; - align-items: left; - gap: 2rem; -} \ No newline at end of file + align-items: center; + + &__spinner { + margin-right: 7px; + display: flex; + } + } + + &__title--inactive { + margin-left: 7px; + color: $application-down-color; + } + + &__action-button { + background: none; + border: none; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; + + &--inactive { + cursor: not-allowed; + pointer-events: none; + + .svg-icon { + background-color: $component-border-color; + } + } + } +} diff --git a/client/src/pages/Reports/Reports.tsx b/client/src/pages/Reports/Reports.tsx index 8f259b7d..9eff01eb 100644 --- a/client/src/pages/Reports/Reports.tsx +++ b/client/src/pages/Reports/Reports.tsx @@ -13,18 +13,13 @@ import PageTemplate from 'components/PageTemplate/PageTemplate'; import HeaderWithIcon from 'components/PageTemplate/components/HeaderWithIcon/HeaderWithIcon'; import LinkComponent from 'components/LinkComponent/LinkComponent.tsx'; import Spinner from 'components/Spinner/Spinner.tsx'; -import {dateFromTimestampMs} from 'lib/date.ts'; +import {dateFromTimestampMs, dateTimeWithoutSecondsFromTimestampMs} from 'lib/date.ts'; import './Reports.scss'; -import CustomTag from 'components/CustomTag/CustomTag.tsx'; const Reports = () => { const [rowsOnDemand, setRowsOnDemand] = useState([]); const [rowsScheduled, setRowsScheduled] = useState([]); - const [rowsAwaitingGeneration, setRowsAwaitingGeneration] = - useState([]); - const [loadingOnDemand, setLoadingOnDemand] = useState(true); - const [loadingScheduled, setLoadingScheduled] = useState(true); - const [loadingAwaitGeneration, setLoadingAwaitGeneration] = useState(true); + const [loading, setLoading] = useState(true); const navigate = useNavigate(); const handleRowClick = (id: string) => { @@ -35,59 +30,88 @@ const Reports = () => { { header: 'Cluster', columnKey: 'clusterId', - customComponent: (row: ReportSummary) => ( - handleRowClick(row.id)}> + customComponent: (row: ReportSummary) => + {row.clusterId} + }, + { + header: 'Title', + columnKey: 'title', + customComponent: (row: ReportSummary) => ( +
+ {row.urgency === null && ( +
+ +
+ )} + + {row.title} + +
), }, - {header: 'Title', columnKey: 'title'}, { header: 'Urgency', columnKey: 'urgency', - customComponent: (row: ReportSummary) => ( - - ), + customComponent: (row: ReportSummary) => + row.urgency ? : null, }, - {header: 'Start date', columnKey: 'startDate'}, - {header: 'End date', columnKey: 'endDate'}, - ]; - - const columnsGenerating: Array> = [ { - header: 'Cluster', - columnKey: 'clusterId', - customComponent: (row: ReportAwaitingGeneration) => ( - - {row.clusterId} - + header: 'Date Range', + columnKey: 'dateRange', + customComponent: (row: ReportSummary) => ( + + {row.startDate} - {row.endDate} + ), }, { - header: 'Report Type', - columnKey: 'reportType', - customComponent: (row: ReportAwaitingGeneration) => , + header: 'Requested at', + columnKey: 'requestedAtDate', }, - {header: 'Start date', columnKey: 'startDate'}, - {header: 'End date', columnKey: 'endDate'}, + { + header: 'Actions', + columnKey: '', + customComponent: (row: ReportSummary) => ( + + ), + } + , ]; const fetchReportsOnDemand = async () => { try { - const reports = await ManagmentServiceApiInstance.getReports('ON-DEMAND'); + const reports = await ManagmentServiceApiInstance.getReports('ON_DEMAND'); const mappedReports = reports.map((report: ReportSummary) => ({ ...report, startDate: dateFromTimestampMs(report.sinceMs), endDate: dateFromTimestampMs(report.toMs), + requestedAtDate: dateTimeWithoutSecondsFromTimestampMs(report.requestedAtMs), })); - setRowsOnDemand(mappedReports); + + setRowsOnDemand(prev => [ + ...mappedReports, + ...prev, + ]); } catch (error) { console.error('Error fetching on-demand reports:', error); - } finally { - setLoadingOnDemand(false); } }; + const fetchReportsScheduled = async () => { try { const reports = await ManagmentServiceApiInstance.getReports('SCHEDULED'); @@ -95,12 +119,15 @@ const Reports = () => { ...report, startDate: dateFromTimestampMs(report.sinceMs), endDate: dateFromTimestampMs(report.toMs), + requestedAtDate: dateTimeWithoutSecondsFromTimestampMs(report.requestedAtMs), })); - setRowsScheduled(mappedReports); + + setRowsScheduled(prev => [ + ...mappedReports, + ...prev, + ]); } catch (error) { console.error('Error fetching scheduled reports:', error); - } finally { - setLoadingScheduled(false); } }; @@ -109,21 +136,43 @@ const Reports = () => { const reports = await ManagmentServiceApiInstance.getAwaitingGenerationReports(); const mappedReports = reports.map((report: ReportAwaitingGeneration) => ({ ...report, + id: `${report.clusterId}-${report.sinceMs}`, + title: 'Awaiting generation...', startDate: dateFromTimestampMs(report.sinceMs), endDate: dateFromTimestampMs(report.toMs), + urgency: null, + requestedAtMs: Date.now(), + requestedAtDate: dateTimeWithoutSecondsFromTimestampMs(Date.now()), })); - setRowsAwaitingGeneration(mappedReports); + + const updateRows = + (filterType: string, setRows: React.Dispatch>) => { + const filteredReports = mappedReports.filter(report => report.reportType === filterType); + setRows(prev => [...filteredReports, ...prev]); + }; + + updateRows('ON_DEMAND', setRowsOnDemand); + updateRows('SCHEDULED', setRowsScheduled); } catch (error) { console.error('Error fetching generating reports:', error); - } finally { - setLoadingAwaitGeneration(false); } }; useEffect(() => { - fetchReportsOnDemand(); - fetchReportsScheduled(); - fetchReportAwaitingGenerations(); + const fetchAllReports = async () => { + setLoading(true); + await Promise.all([ + fetchReportsOnDemand(), + fetchReportsScheduled(), + fetchReportAwaitingGenerations(), + ]); + + setRowsOnDemand(prev => [...prev].sort((a, b) => b.requestedAtMs - a.requestedAtMs)); + setRowsScheduled(prev => [...prev].sort((a, b) => b.requestedAtMs - a.requestedAtMs)); + + setLoading(false); + }; + fetchAllReports(); }, []); return ( @@ -136,24 +185,11 @@ const Reports = () => { } >
- {rowsAwaitingGeneration.length > 0 && ( - } - title={'Reports awaiting generation'} - > - {loadingAwaitGeneration ? ( - - ) : ( -
- )} - - )} - } - title={'Generated reports scheduled'} + title={'Scheduled reports'} > - {loadingScheduled ? ( + {loading ? ( ) : rowsScheduled.length === 0 ? ( <> @@ -167,9 +203,9 @@ const Reports = () => { } - title={'Generated reports on demand'} + title={'Reports on demand'} > - {loadingOnDemand ? ( + {loading ? ( ) : rowsOnDemand.length === 0 ? ( <> diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index a9b358c4..e050498d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -39,7 +39,7 @@ services: - action: rebuild path: ./go/pkg target: /src - + pod-agent: user: "0" # Elevated permission needed for bind mount container_name: magpie-monitor-pod-agent diff --git a/docker-compose.yml b/docker-compose.yml index cf2cabad..e19c3cfc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -241,7 +241,7 @@ services: management-pgadmin: image: dpage/pgadmin4 - ports: + ports: - "${MANAGEMENT_PGADMIN_PORT}:80" environment: - PGADMIN_DEFAULT_EMAIL=${MANAGEMENT_PGADMIN_MAIL} diff --git a/management-service/src/main/java/pl/pwr/zpi/reports/api/ReportsController.java b/management-service/src/main/java/pl/pwr/zpi/reports/api/ReportsController.java index 87965e33..33624979 100644 --- a/management-service/src/main/java/pl/pwr/zpi/reports/api/ReportsController.java +++ b/management-service/src/main/java/pl/pwr/zpi/reports/api/ReportsController.java @@ -58,8 +58,8 @@ public ResponseEntity> getReportsOnDemand(@RequestParam S } @GetMapping("/await-generation") - public ResponseEntity> getGenerationReports() { - return ResponseEntity.ok(reportsService.getGenerationReports()); + public ResponseEntity> getAwaitingGenerationReports() { + return ResponseEntity.ok(reportsService.getAwaitingGenerationReports()); } @GetMapping("/{id}") diff --git a/management-service/src/main/java/pl/pwr/zpi/reports/service/ReportsService.java b/management-service/src/main/java/pl/pwr/zpi/reports/service/ReportsService.java index 6e474d31..2df8081b 100644 --- a/management-service/src/main/java/pl/pwr/zpi/reports/service/ReportsService.java +++ b/management-service/src/main/java/pl/pwr/zpi/reports/service/ReportsService.java @@ -36,7 +36,7 @@ public List getFailedReportGenerationRequests() return reportGenerationRequestMetadataRepository.findByStatus(ReportGenerationStatus.ERROR); } - public List getGenerationReports() { + public List getAwaitingGenerationReports() { return reportGenerationRequestMetadataRepository .findByStatus(ReportGenerationStatus.GENERATING) .stream()