diff --git a/locales/en/plugin__camel-openshift-console-plugin.json b/locales/en/plugin__camel-openshift-console-plugin.json index bee788f..5913d1d 100644 --- a/locales/en/plugin__camel-openshift-console-plugin.json +++ b/locales/en/plugin__camel-openshift-console-plugin.json @@ -1,8 +1,14 @@ { - "C": "C", + "{{count}} day": "{{count}} day", + "{{count}} day_plural": "{{count}} day", + "{{count}} hour": "{{count}} hour", + "{{count}} hour_plural": "{{count}} hour", + "{{count}} minute": "{{count}} minute", + "{{count}} minute_plural": "{{count}} minute", "Camel": "Camel", "Camel App Details": "Camel App Details", - "Camel Details": "Camel Details", + "Camel Application": "Camel Application", + "Camel Applications": "Camel Applications", "Camel Version": "Camel Version", "Details": "Details", "Endpoints": "Endpoints", @@ -11,6 +17,7 @@ "Health": "Health", "Image": "Image", "Internal IP": "Internal IP", + "Just now": "Just now", "Kind": "Kind", "Location:": "Location:", "Metrics": "Metrics", @@ -30,5 +37,7 @@ "succeed": "succeed", "total": "total", "unknown host": "unknown host", + "Uptime": "Uptime", + "View Hawtio": "View Hawtio", "View Logs": "View Logs" } \ No newline at end of file diff --git a/src/components/camel-app-details/CamelAppStatusPod.tsx b/src/components/camel-app-details/CamelAppStatusPod.tsx index 6c02dbb..1d71ca9 100644 --- a/src/components/camel-app-details/CamelAppStatusPod.tsx +++ b/src/components/camel-app-details/CamelAppStatusPod.tsx @@ -14,6 +14,7 @@ import { podGVK } from '../../const'; import { ResourceLink, ResourceStatus } from '@openshift-console/dynamic-plugin-sdk'; import { useTranslation } from 'react-i18next'; import Status from '@openshift-console/dynamic-plugin-sdk/lib/app/components/status/Status'; +import { formatDuration } from '../../date-utils'; type CamelAppStatusPodProps = { obj: CamelAppKind; @@ -23,6 +24,9 @@ type CamelAppStatusPodProps = { const CamelAppStatusPod: React.FC = ({ obj: camelInt, pod: camelPod }) => { const { t } = useTranslation('plugin__camel-openshift-console-plugin'); + // Golang time.Time is in nanoseconds + const durationFull = formatDuration(Number(camelPod.uptime) / 1000000, { omitSuffix: false }); + return ( <> @@ -48,6 +52,10 @@ const CamelAppStatusPod: React.FC = ({ obj: camelInt, po {t('Internal IP')}: {camelPod.internalIp} + + {t('Uptime')}: + {durationFull} + {t('Runtime')}: diff --git a/src/components/camel-app-resources/CamelAppPods.tsx b/src/components/camel-app-resources/CamelAppPods.tsx index a3d7a78..272a7e5 100644 --- a/src/components/camel-app-resources/CamelAppPods.tsx +++ b/src/components/camel-app-resources/CamelAppPods.tsx @@ -4,7 +4,7 @@ import { K8sResourceKind, ResourceLink } from '@openshift-console/dynamic-plugin import { podGVK } from '../../const'; import Status from '@openshift-console/dynamic-plugin-sdk/lib/app/components/status/Status'; import { useCamelAppPods } from './useCamelAppResources'; -import { getPodStatus } from '../../utils'; +import { getPodStatus } from './podStatus'; import { useTranslation } from 'react-i18next'; import { isHawtioEnabled, useHawtioConsolePlugin } from './useHawtio'; @@ -82,7 +82,9 @@ const CamelAppPods: React.FC = ({ obj: camelInt }) => { <> - + {t('View Logs')} @@ -100,7 +102,9 @@ const CamelAppPods: React.FC = ({ obj: camelInt }) => { ) : ( - + {t('View Logs')} diff --git a/src/components/camel-app-resources/podStatus.ts b/src/components/camel-app-resources/podStatus.ts new file mode 100644 index 0000000..103993c --- /dev/null +++ b/src/components/camel-app-resources/podStatus.ts @@ -0,0 +1,80 @@ +import * as _ from 'lodash'; + +const isContainerFailedFilter = (containerStatus) => { + return containerStatus.state.terminated && containerStatus.state.terminated.exitCode !== 0; +}; + +export const isContainerLoopingFilter = (containerStatus) => { + return ( + containerStatus.state.waiting && containerStatus.state.waiting.reason === 'CrashLoopBackOff' + ); +}; + +const numContainersReadyFilter = (pod) => { + const { + status: { containerStatuses }, + } = pod; + let numReady = 0; + _.forEach(containerStatuses, (status) => { + if (status.ready) { + numReady++; + } + }); + return numReady; +}; + +const isReady = (pod) => { + const { + spec: { containers }, + } = pod; + const numReady = numContainersReadyFilter(pod); + const total = _.size(containers); + + return numReady === total; +}; + +const podWarnings = (pod) => { + const { + status: { phase, containerStatuses }, + } = pod; + if (phase === 'Running' && containerStatuses) { + return _.map(containerStatuses, (containerStatus) => { + if (!containerStatus.state) { + return null; + } + + if (isContainerFailedFilter(containerStatus)) { + if (_.has(pod, ['metadata', 'deletionTimestamp'])) { + return 'Failed'; + } + return 'Warning'; + } + if (isContainerLoopingFilter(containerStatus)) { + return 'CrashLoopBackOff'; + } + return null; + }).filter((x) => x); + } + return null; +}; + +export const getPodStatus = (pod) => { + if (_.has(pod, ['metadata', 'deletionTimestamp'])) { + return 'Terminating'; + } + const warnings = podWarnings(pod); + if (warnings !== null && warnings.length) { + if (warnings.includes('CrashLoopBackOff')) { + return 'CrashLoopBackOff'; + } + if (warnings.includes('Failed')) { + return 'Failed'; + } + return 'Warning'; + } + const phase = _.get(pod, ['status', 'phase'], 'Unknown'); + if (phase === 'Running' && !isReady(pod)) { + return 'NotReady'; + } + return phase; +}; diff --git a/src/const.ts b/src/const.ts index c8017f5..cb968e6 100644 --- a/src/const.ts +++ b/src/const.ts @@ -2,21 +2,6 @@ import { K8sGroupVersionKind } from '@openshift-console/dynamic-plugin-sdk'; export const HAWTIO_CONSOLE_PLUGIN_NAME = 'hawtio-online-console-plugin'; -export const METADATA_LABEL_SELECTOR_CAMEL_APP_KEY = 'camel/integration-runtime'; -export const METADATA_LABEL_SELECTOR_CAMEL_APP_VALUE = 'camel'; - -export const METADATA_ANNOTATION_APP_VERSION = 'app.kubernetes.io/version'; - -export const METADATA_ANNOTATION_CAMEL_VERSION = 'camel/camel-core-version'; - -export const METADATA_ANNOTATION_CAMEL_QUARKUS_PLATFORM_VERSION = 'camel/quarkus-platform'; -export const METADATA_ANNOTATION_CAMEL_CEQ_VERSION = 'camel/camel-quarkus'; - -export const METADATA_ANNOTATION_QUARKUS_BUILD_TIMESTAMP = 'app.quarkus.io/build-timestamp'; - -export const METADATA_ANNOTATION_CAMEL_SPRINGBOOT_VERSION = 'camel/spring-boot-version'; -export const METADATA_ANNOTATION_CAMEL_CSB_VERSION = 'camel/camel-spring-boot-version'; - export const camelAppGVK: K8sGroupVersionKind = { group: 'camel.apache.org', version: 'v1alpha1', @@ -83,3 +68,4 @@ export const consolePluginGVK: K8sGroupVersionKind = { }; export const ALL_NAMESPACES_KEY = '#ALL_NS#'; +export const LAST_LANGUAGE_LOCAL_STORAGE_KEY = 'bridge/last-language'; diff --git a/src/date-utils.ts b/src/date-utils.ts new file mode 100644 index 0000000..d660834 --- /dev/null +++ b/src/date-utils.ts @@ -0,0 +1,89 @@ +import { useTranslation } from 'react-i18next'; +import { getLastLanguage } from './utils'; + +export type Duration = { + days: number; + hours: number; + minutes: number; + seconds: number; +}; + +export const relativeTimeFormatter = (langArg: string) => + Intl.RelativeTimeFormat ? new Intl.RelativeTimeFormat(langArg) : null; + +export const dateTimeFormatter = (langArg: string) => + new Intl.DateTimeFormat(langArg, { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + year: 'numeric', + }); + +function getDuration(ms: number): Duration { + let seconds = Math.floor(ms / 1000); + let minutes = Math.floor(seconds / 60); + seconds %= 60; + let hours = Math.floor(minutes / 60); + minutes %= 60; + const days = Math.floor(hours / 24); + hours %= 24; + return { days, hours, minutes, seconds }; +} + +export const formatDuration = (ms: number, options?) => { + const { t } = useTranslation('plugin__camel-openshift-console-plugin'); + const langArg = getLastLanguage(); + const duration = getDuration(ms); + + // Check for null. If dateTime is null, it returns incorrect date Jan 1 1970. + if (!duration) { + return '-'; + } + + const d = new Date(ms); + const justNow = t('Just now'); + + // If the event occurred less than one minute in the future, assume it's clock drift and show "Just now." + if (!options?.omitSuffix && ms < 60000 && ms > -60000) { + return justNow; + } + + // Do not attempt to handle other dates in the future. + if (ms < 0) { + return '-'; + } + + const { days, hours, minutes } = getDuration(ms); + + if (options?.omitSuffix) { + if (days) { + return t('{{count}} day', { count: days }); + } + if (hours) { + return t('{{count}} hour', { count: hours }); + } + return t('{{count}} minute', { count: minutes }); + } + + // Fallback to normal date/time formatting if Intl.RelativeTimeFormat is not + // available. This is the case for older Safari versions. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat#browser_compatibility + if (!relativeTimeFormatter(langArg)) { + return dateTimeFormatter(langArg).format(d); + } + + if (!days && !hours && !minutes) { + return justNow; + } + + if (days) { + return relativeTimeFormatter(langArg).format(-days, 'day'); + } + + if (hours) { + return relativeTimeFormatter(langArg).format(-hours, 'hour'); + } + + return relativeTimeFormatter(langArg).format(-minutes, 'minute'); +}; diff --git a/src/types.ts b/src/types.ts index a5805b1..77d3402 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,6 +11,8 @@ export type CamelAppKind = K8sResourceKind & { export type CamelAppStatusPod = { name: string; internalIp: string; + /** Format: date-time - in nanoseconds */ + uptime: string; observe: CamelAppObservability; ready: boolean; runtime: CamelAppRuntime; diff --git a/src/utils.ts b/src/utils.ts index eb3964e..55d58af 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,14 +1,14 @@ -import * as _ from 'lodash'; import { cronJobGVK, deploymentConfigGVK, deploymentGVK, - METADATA_ANNOTATION_APP_VERSION, - METADATA_ANNOTATION_QUARKUS_BUILD_TIMESTAMP, + LAST_LANGUAGE_LOCAL_STORAGE_KEY, } from './const'; -import { CamelAppKind } from './types'; import { K8sResourceKind } from '@openshift-console/dynamic-plugin-sdk'; +export const getLastLanguage = (): string => + localStorage.getItem(LAST_LANGUAGE_LOCAL_STORAGE_KEY) ?? navigator.language; + export function CamelAppOwnerGVK(kind: string) { switch (kind) { case deploymentConfigGVK.kind: @@ -20,31 +20,6 @@ export function CamelAppOwnerGVK(kind: string) { } } -export function getAppVersion(app: CamelAppKind): string | null { - if (app && app.metadata) { - return app.metadata.annotations?.[METADATA_ANNOTATION_APP_VERSION]; - } - return null; -} - -export function getBuildTimestamp(app: CamelAppKind): string | null { - if (app && app.metadata) { - return app.metadata.annotations?.[METADATA_ANNOTATION_QUARKUS_BUILD_TIMESTAMP]; - } - return null; -} - -export function getHealthEndpoints(framework: string): string[] { - switch (framework) { - case 'quarkus': - return ['/observe/health/live', '/observe/health/ready', '/observe/health/started']; - case 'Spring-Boot': - return ['/observe/health/liveness', '/observe/health/readiness']; - default: - return []; - } -} - // TODO use something else than Unknown export function serviceMatchLabelValue(camelAppOwner: K8sResourceKind): string { if (camelAppOwner.kind == 'Deployment') { @@ -56,84 +31,3 @@ export function serviceMatchLabelValue(camelAppOwner: K8sResourceKind): string { } return 'Unknown'; } - -// Pods status utils - -const isContainerFailedFilter = (containerStatus) => { - return containerStatus.state.terminated && containerStatus.state.terminated.exitCode !== 0; -}; - -export const isContainerLoopingFilter = (containerStatus) => { - return ( - containerStatus.state.waiting && containerStatus.state.waiting.reason === 'CrashLoopBackOff' - ); -}; - -const numContainersReadyFilter = (pod) => { - const { - status: { containerStatuses }, - } = pod; - let numReady = 0; - _.forEach(containerStatuses, (status) => { - if (status.ready) { - numReady++; - } - }); - return numReady; -}; - -const isReady = (pod) => { - const { - spec: { containers }, - } = pod; - const numReady = numContainersReadyFilter(pod); - const total = _.size(containers); - - return numReady === total; -}; - -const podWarnings = (pod) => { - const { - status: { phase, containerStatuses }, - } = pod; - if (phase === 'Running' && containerStatuses) { - return _.map(containerStatuses, (containerStatus) => { - if (!containerStatus.state) { - return null; - } - - if (isContainerFailedFilter(containerStatus)) { - if (_.has(pod, ['metadata', 'deletionTimestamp'])) { - return 'Failed'; - } - return 'Warning'; - } - if (isContainerLoopingFilter(containerStatus)) { - return 'CrashLoopBackOff'; - } - return null; - }).filter((x) => x); - } - return null; -}; - -export const getPodStatus = (pod) => { - if (_.has(pod, ['metadata', 'deletionTimestamp'])) { - return 'Terminating'; - } - const warnings = podWarnings(pod); - if (warnings !== null && warnings.length) { - if (warnings.includes('CrashLoopBackOff')) { - return 'CrashLoopBackOff'; - } - if (warnings.includes('Failed')) { - return 'Failed'; - } - return 'Warning'; - } - const phase = _.get(pod, ['status', 'phase'], 'Unknown'); - if (phase === 'Running' && !isReady(pod)) { - return 'NotReady'; - } - return phase; -};