Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ACM-3456 Governance > Policies > Results tab #3857

Draft
wants to merge 36 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e60f581
adds translations
vishsanghishetty Sep 13, 2024
ee759a3
adds localization support to i18n
vishsanghishetty Sep 13, 2024
6699200
adds i18nextLng key to constants
vishsanghishetty Sep 13, 2024
8cf7ebd
Add functions for date and time formatting, including relative time s…
vishsanghishetty Sep 13, 2024
5678146
Add localization support and relative time formatting for creation ti…
vishsanghishetty Sep 13, 2024
34e21a5
Add localization support and relative time formatting for creation ti…
vishsanghishetty Sep 13, 2024
4cd1142
Implement function to retrieve the last selected language from localS…
vishsanghishetty Sep 13, 2024
98d749b
prettier fix
vishsanghishetty Sep 13, 2024
f9476bc
adds tests datetime
vishsanghishetty Sep 13, 2024
f276ea3
addresses sonar cloud issue
vishsanghishetty Sep 13, 2024
c685d97
Extracts the nested ternary operation into an independent statement.
vishsanghishetty Sep 13, 2024
340b7c5
merge main
vishsanghishetty Sep 14, 2024
2d8c2c4
unit test for twenty four hour time
vishsanghishetty Sep 14, 2024
5376e82
adds 2 more test cases in unit tests
vishsanghishetty Sep 14, 2024
63f7c2f
fixes failing unit test
vishsanghishetty Sep 14, 2024
b8eb2f7
fixes date formatter test
vishsanghishetty Sep 14, 2024
929a4c7
fixes missing transalations
vishsanghishetty Sep 14, 2024
f497f7b
fixes prettier issue
vishsanghishetty Sep 14, 2024
df528fc
removes commented code
vishsanghishetty Sep 14, 2024
44d30a4
removes unused function
vishsanghishetty Sep 14, 2024
396c553
fixes prettier issue
vishsanghishetty Sep 14, 2024
6a668e7
removes commented line
vishsanghishetty Sep 14, 2024
9f29273
removes console log
vishsanghishetty Sep 16, 2024
eac6e70
Add default values for history message and timestamp
vishsanghishetty Sep 16, 2024
bd17975
addresses comment space
vishsanghishetty Sep 16, 2024
b05661e
deletes date time implementation
vishsanghishetty Sep 17, 2024
ac62c1a
updates translation strings
vishsanghishetty Sep 17, 2024
a349081
Update i18n interpolation object implementation for date and number f…
vishsanghishetty Sep 17, 2024
a3a2a47
adds i18n tests
vishsanghishetty Sep 17, 2024
c4dfed2
removes earlier const used for local storage i18n
vishsanghishetty Sep 17, 2024
bd8582d
Implement i18n date localization for Credentials Page
vishsanghishetty Sep 17, 2024
09b6e4c
Implement i18n date localization for Policy Results table
vishsanghishetty Sep 17, 2024
8f1e618
updates i18n tests
vishsanghishetty Sep 17, 2024
16bcd06
updates i18n tests
vishsanghishetty Sep 17, 2024
e56a192
updates i18n tests
vishsanghishetty Sep 17, 2024
380881c
updates i18n tests and i18n
vishsanghishetty Sep 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@
"{{count}} more_plural": "{{count}} more",
"{{count}} selected": "{{count}} selected",
"{{count}} selected_plural": "{{count}} selected",
"{{date, format}}": "{{date, format}}",
"{{date, fromNow}}": "{{date, fromNow}}",
"{{kind}} details": "{{kind}} details",
"{{name}} was successfully created.": "{{name}} was successfully created.",
"{{name}} was successfully updated.": "{{name}} was successfully updated.",
"{{names}} were successfully synced.": "{{names}} were successfully synced.",
"{{namespace}} has been successfully added to Argo server.": "{{namespace}} has been successfully added to Argo server.",
"{{number}}": "{{number}}",
"{{remaining}} more": "{{remaining}} more",
"{{remoteCount}} Remote": "{{remoteCount}} Remote",
"{{remoteCount}} Remote, 1 Local": "{{remoteCount}} Remote, 1 Local",
Expand Down
2 changes: 2 additions & 0 deletions frontend/public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,7 @@
"Discover hosts to create host inventory": "Descubrir hosts para crear un inventario de hosts",
"Discovered": "Descubierto",
"Discovered clusters": "Clústeres descubiertos",
"Discovered policies": "Políticas descubiertas",
"discovery.addDiscovery": "Crear ajustes de descubrimiento",
"discovery.configureDiscovery": "Configurar los ajustes de descubrimiento",
"discovery.import": "Importar clúster",
Expand Down Expand Up @@ -1612,6 +1613,7 @@
"Job template": "Plantilla de tareas",
"Joined": "Unido",
"Jump to the bottom": "Saltar a la parte inferior",
"Just now": "Justo ahora",
"Kind": "Clase",
"Kubernetes": "Kubernetes",
"Kubernetes type": "Tipo Kubernetes",
Expand Down
2 changes: 2 additions & 0 deletions frontend/public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,7 @@
"Discover hosts to create host inventory": "Découvrir des hôtes pour créer un inventaire d’hôtes",
"Discovered": "Découvert",
"Discovered clusters": "Clusters découverts",
"Discovered policies": "Stratégies découvertes",
"discovery.addDiscovery": "Créer des paramètres de découverte",
"discovery.configureDiscovery": "Configurer les paramètres de découverte",
"discovery.import": "Importer un cluster",
Expand Down Expand Up @@ -1612,6 +1613,7 @@
"Job template": "Modèle de tâche",
"Joined": "Rejoint",
"Jump to the bottom": "Aller en bas",
"Just now": "À l'instant",
"Kind": "Type",
"Kubernetes": "Kubernetes",
"Kubernetes type": "Type Kubernetes",
Expand Down
2 changes: 2 additions & 0 deletions frontend/public/locales/ja/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,7 @@
"Discover hosts to create host inventory": "ホストを検出してホストインベントリーを作成する",
"Discovered": "検出済み",
"Discovered clusters": "検出されたクラスター",
"Discovered policies": "検出されたポリシー",
"discovery.addDiscovery": "検出設定の作成",
"discovery.configureDiscovery": "検出設定の設定",
"discovery.import": "クラスターのインポート",
Expand Down Expand Up @@ -1598,6 +1599,7 @@
"Job template": "ジョブテンプレート",
"Joined": "参加済み",
"Jump to the bottom": "下にジャンプ",
"Just now": "たった今",
"Kind": "種類",
"Kubernetes": "Kubernetes",
"Kubernetes type": "Kubernetes タイプ",
Expand Down
2 changes: 2 additions & 0 deletions frontend/public/locales/ko/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,7 @@
"Discover hosts to create host inventory": "호스트 인벤토리를 생성할 호스트 검색",
"Discovered": "검색됨",
"Discovered clusters": "검색된 클러스터",
"Discovered policies": "검색된 정책",
"discovery.addDiscovery": "검색 설정 생성",
"discovery.configureDiscovery": "검색 설정 구성",
"discovery.import": "클러스터 가져오기",
Expand Down Expand Up @@ -1598,6 +1599,7 @@
"Job template": "작업 템플릿",
"Joined": "참여됨",
"Jump to the bottom": "맨 아래로 이동",
"Just now": "방금",
"Kind": "유형",
"Kubernetes": "Kubernetes",
"Kubernetes type": "Kubernetes 유형",
Expand Down
2 changes: 2 additions & 0 deletions frontend/public/locales/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,7 @@
"Discover hosts to create host inventory": "发现主机以创建主机清单",
"Discovered": "发现的",
"Discovered clusters": "发现的集群",
"Discovered policies": "发现的策略",
"discovery.addDiscovery": "创建发现设置",
"discovery.configureDiscovery": "配置发现设置",
"discovery.import": "导入集群",
Expand Down Expand Up @@ -1598,6 +1599,7 @@
"Job template": "作业模板",
"Joined": "Joined",
"Jump to the bottom": "跳转到底部",
"Just now": "刚刚",
"Kind": "种类(Kind)",
"Kubernetes": "Kubernetes",
"Kubernetes type": "Kubernetes 类型",
Expand Down
141 changes: 141 additions & 0 deletions frontend/src/lib/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/* Copyright Contributors to the Open Cluster Management project */

import { t } from 'i18next'
import i18n from 'i18next'
import { jest } from '@jest/globals'

jest.mock('i18next', () => ({
t: jest.fn((key, { date, number }) => {
if (date instanceof Date) {
if (typeof key === 'string' && key.includes('fromNow')) {
const now = new Date()
const elapsed = now.getTime() - date.getTime()
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })

const seconds = Math.floor(elapsed / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)

if (seconds < 60) return rtf.format(-seconds, 'second')
if (minutes < 60) return rtf.format(-minutes, 'minute')
if (hours < 24) return rtf.format(-hours, 'hour')
return rtf.format(-days, 'day')
}
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
}).format(date)
}

if (typeof number === 'number') {
return new Intl.NumberFormat('en').format(number)
}

return ''
}),
}))

describe('Date formatting tests', () => {
const now = new Date('2024-09-15T12:00:00Z') // Use a fixed reference time

beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2024-09-15T12:00:00Z'))
})

afterEach(() => {
jest.useRealTimers()
})

it('should return "5 seconds ago" for timestamps 5 seconds ago', () => {
const timestamp = new Date(now.getTime() - 5000) // 5 seconds ago
const result = t('{{date, fromNow}}', { date: timestamp })
expect(result).toBe('5 seconds ago') // Expected output
})

it('should return "1 minute ago" for timestamps 1 minute ago', () => {
const timestamp = new Date(now.getTime() - 60000) // 1 minute ago
const result = t('{{date, fromNow}}', { date: timestamp })
expect(result).toBe('1 minute ago') // Expected output
})

it('should return "1 hour ago" for timestamps 1 hour ago', () => {
const timestamp = new Date(now.getTime() - 3600000) // 1 hour ago
const result = t('{{date, fromNow}}', { date: timestamp })
expect(result).toBe('1 hour ago') // Expected output
})

it('should return "yesterday" for timestamps 1 day ago', () => {
const timestamp = new Date(now.getTime() - 86400000) // 1 day ago
const result = t('{{date, fromNow}}', { date: timestamp })
expect(result).toBe('yesterday') // Adjusted expected output
})

it('should return "2 days ago" for timestamps 2 days ago', () => {
const timestamp = new Date(now.getTime() - 172800000) // 2 days ago
const result = t('{{date, fromNow}}', { date: timestamp })
expect(result).toBe('2 days ago') // Expected output
})
})

describe('i18n format function', () => {
const now = new Date('2024-09-15T12:00:00Z') // Reference time

beforeEach(() => {
jest.useFakeTimers().setSystemTime(now)
})

afterEach(() => {
jest.useRealTimers()
})

test('should format date with default settings', () => {
const date = new Date('2024-09-14T12:00:00Z') // UTC time
const result = i18n.t('{{date, format}}', { date: date })

// Adjusting expected result based on how i18n formats the date
const expected = 'Sep 14, 2024, 12:00:00 PM' // Adjusted for UTC output

expect(result).toBe(expected)
})

test('should return "5 seconds ago" for timestamps 5 seconds ago', () => {
const timestamp = new Date(now.getTime() - 5000)
const result = i18n.t('{{date, fromNow}}', { date: timestamp })
expect(result).toBe('5 seconds ago')
})

test('should return "1 minute ago" for timestamps 1 minute ago', () => {
const timestamp = new Date(now.getTime() - 60000)
const result = i18n.t('{{date, fromNow}}', { date: timestamp })
expect(result).toBe('1 minute ago')
})

test('should return "1 hour ago" for timestamps 1 hour ago', () => {
const timestamp = new Date(now.getTime() - 3600000)
const result = i18n.t('{{date, fromNow}}', { date: timestamp })
expect(result).toBe('1 hour ago')
})

test('should return "yesterday" for timestamps 1 day ago', () => {
const timestamp = new Date(now.getTime() - 86400000)
const result = i18n.t('{{date, fromNow}}', { date: timestamp })
expect(result).toBe('yesterday')
})

test('should return "2 days ago" for timestamps 2 days ago', () => {
const timestamp = new Date(now.getTime() - 172800000)
const result = i18n.t('{{date, fromNow}}', { date: timestamp })
expect(result).toBe('2 days ago')
})

test('should format numbers correctly', () => {
const number = 1234567.89
const result = i18n.t('{{number}}', { number })
expect(result).toBe('1,234,567.89') // Adjusts based on the locale
})
})
48 changes: 37 additions & 11 deletions frontend/src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,54 @@ import HttpApi from 'i18next-http-backend'
import { supportedLanguages } from './supportedLanguages'

i18n
// pass the i18n instance to react-i18next
.use(initReactI18next)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// fetch json files
// learn more: https://github.com/i18next/i18next-http-backend
.use(HttpApi)
// init i18next
.init({
backend: {
loadPath: '/multicloud/locales/{{lng}}/{{ns}}.json',
},
compatibilityJSON: 'v3',
fallbackLng: ['en'], // if language is not supported or string is missing, fallback to English
keySeparator: false, // this repo will use single level json
fallbackLng: ['en'],
keySeparator: false,
interpolation: {
escapeValue: false, // react handles this already
escapeValue: false,
format: (value, format, lng) => {
if (value instanceof Date) {
if (format === 'fromNow') {
const now = new Date()
const elapsed = now.getTime() - value.getTime()
const rtf = new Intl.RelativeTimeFormat(lng, { numeric: 'auto' })

const seconds = Math.floor(elapsed / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)

if (seconds < 60) return rtf.format(-seconds, 'second')
if (minutes < 60) return rtf.format(-minutes, 'minute')
if (hours < 24) return rtf.format(-hours, 'hour')
return rtf.format(-days, 'day')
}
return new Intl.DateTimeFormat(lng, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: 'America/New_York', // Example for EST
}).format(value)
}
if (typeof value === 'number') {
return new Intl.NumberFormat(lng).format(value)
}
return value
},
},
defaultNS: 'translation', // the default file for strings when using useTranslation, etc
defaultNS: 'translation',
nsSeparator: '~',
supportedLngs: supportedLanguages, // only languages from this array will attempt to be loaded
supportedLngs: supportedLanguages,
simplifyPluralSuffix: true,
})

Expand Down
7 changes: 4 additions & 3 deletions frontend/src/routes/Credentials/CredentialsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
Provider,
ProviderLongTextMap,
} from '../../ui-components'
import moment from 'moment'
import { Fragment, useEffect, useMemo, useState } from 'react'
import { Link, generatePath, useNavigate } from 'react-router-dom-v5-compat'
import { useRecoilValue, useSharedAtoms } from '../../shared-recoil'
Expand Down Expand Up @@ -236,15 +235,17 @@ export function CredentialsTable(props: {
sort: 'metadata.creationTimestamp',
cell: (resource) => (
<span style={{ whiteSpace: 'nowrap' }}>
{resource.metadata.creationTimestamp && moment(new Date(resource.metadata.creationTimestamp)).fromNow()}
{resource.metadata.creationTimestamp &&
t('{{date, fromNow}}', { date: new Date(resource.metadata.creationTimestamp) })}
</span>
),
exportContent: (item: Secret) => {
if (item.metadata.creationTimestamp) {
return moment(new Date(item.metadata.creationTimestamp)).fromNow()
return t('{{date, fromNow}}', { date: new Date(item.metadata.creationTimestamp) })
}
},
},

{
header: '',
cellTransforms: [fitContent],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { PageSection, Title, Tooltip } from '@patternfly/react-core'
import { CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon } from '@patternfly/react-icons'
import { AcmEmptyState, AcmTable, AcmTablePaginationContextProvider, compareStrings } from '../../../../ui-components'
import moment from 'moment'
import { useEffect, useMemo, useState } from 'react'
import { Link, generatePath } from 'react-router-dom-v5-compat'
import { useRecoilValue, useSharedAtoms } from '../../../../shared-recoil'
Expand All @@ -23,7 +22,7 @@ export interface ResultsTableData {
kind: string
status: string
message: string
timestamp: moment.MomentInput
timestamp: string | number | Date
policyName: string
policyNamespace: string
remediationAction: string
Expand Down Expand Up @@ -91,8 +90,8 @@ export default function PolicyDetailsResults() {
apiVersion: template?.objectDefinition.apiVersion ?? '-',
kind: template?.objectDefinition.kind ?? '-',
status: detail.compliant ?? 'no-status',
message: (detail?.history && detail.history[0]?.message) ?? '-',
timestamp: detail?.history && detail?.history[0]?.lastTimestamp,
message: detail?.history?.[0]?.message ?? '-',
timestamp: detail?.history?.[0]?.lastTimestamp ?? '-',
policyName,
policyNamespace,
remediationAction: getPolicyTempRemediation(policyResponse, template),
Expand Down Expand Up @@ -185,14 +184,16 @@ export default function PolicyDetailsResults() {
sort: 'templateName',
cell: (item: ResultsTableData) => {
const templateDetailURL = getTemplateDetailURL(item)
const displayTemplate = templateDetailURL ? (
<span>
<Link to={templateDetailURL}>{item.templateName}</Link>
</span>
) : (
item.templateName
)

return canCreatePolicy ? (
templateDetailURL ? (
<span>
<Link to={templateDetailURL}>{item.templateName}</Link>
</span>
) : (
item.templateName
)
displayTemplate
) : (
<Tooltip content={t('rbac.unauthorized')}>
<span className="link-disabled" id="template-name-link-disabled">
Expand Down Expand Up @@ -259,9 +260,9 @@ export default function PolicyDetailsResults() {
header: t('Last report'),
sort: 'timestamp',
cell: (item: ResultsTableData) =>
item.timestamp ? moment(item.timestamp, 'YYYY-MM-DDTHH:mm:ssZ').fromNow() : '-',
item.timestamp ? t('{{date, fromNow}}', { date: new Date(item.timestamp) }) : '-',
exportContent: (item: ResultsTableData) =>
item.timestamp ? moment(item.timestamp, 'YYYY-MM-DDTHH:mm:ssZ').fromNow() : '-',
item.timestamp ? t('{{date, fromNow}}', { date: new Date(item.timestamp) }) : '-',
},
{
header: t('History'),
Expand Down