From 275765b8fc02945dcd9c22dabe40500d82946cfb Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 3 Oct 2019 18:26:38 -0400 Subject: [PATCH 1/3] Refactor language utility Move the language helper out of RootProvider and into a utilities module so that it can be more easiliy reused where needed. In some cases we want the full language code so that logic has been moved into a separate function. --- awx/ui_next/src/RootProvider.jsx | 11 ++------ awx/ui_next/src/RootProvider.test.jsx | 18 ------------ awx/ui_next/src/util/language.js | 15 ++++++++++ awx/ui_next/src/util/language.test.jsx | 39 ++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 27 deletions(-) delete mode 100644 awx/ui_next/src/RootProvider.test.jsx create mode 100644 awx/ui_next/src/util/language.js create mode 100644 awx/ui_next/src/util/language.test.jsx diff --git a/awx/ui_next/src/RootProvider.jsx b/awx/ui_next/src/RootProvider.jsx index 48fdf3200c..92b3e83776 100644 --- a/awx/ui_next/src/RootProvider.jsx +++ b/awx/ui_next/src/RootProvider.jsx @@ -3,23 +3,16 @@ import { I18nProvider } from '@lingui/react'; import { HashRouter } from 'react-router-dom'; +import { getLanguageWithoutRegionCode } from '@util/language'; import ja from '../build/locales/ja/messages'; import en from '../build/locales/en/messages'; -export function getLanguage(nav) { - const language = - (nav.languages && nav.languages[0]) || nav.language || nav.userLanguage; - const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0]; - - return languageWithoutRegionCode; -} - class RootProvider extends Component { render() { const { children } = this.props; const catalogs = { en, ja }; - const language = getLanguage(navigator); + const language = getLanguageWithoutRegionCode(navigator); return ( diff --git a/awx/ui_next/src/RootProvider.test.jsx b/awx/ui_next/src/RootProvider.test.jsx deleted file mode 100644 index 45b87782f6..0000000000 --- a/awx/ui_next/src/RootProvider.test.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import { getLanguage } from './RootProvider'; - -describe('RootProvider.jsx', () => { - test('getLanguage returns the expected language code', () => { - expect(getLanguage({ languages: ['es-US'] })).toEqual('es'); - expect( - getLanguage({ - languages: ['es-US'], - language: 'fr-FR', - userLanguage: 'en-US', - }) - ).toEqual('es'); - expect(getLanguage({ language: 'fr-FR', userLanguage: 'en-US' })).toEqual( - 'fr' - ); - expect(getLanguage({ userLanguage: 'en-US' })).toEqual('en'); - }); -}); diff --git a/awx/ui_next/src/util/language.js b/awx/ui_next/src/util/language.js new file mode 100644 index 0000000000..1b2b31a8a4 --- /dev/null +++ b/awx/ui_next/src/util/language.js @@ -0,0 +1,15 @@ +export function getLanguage(nav) { + if (nav.languages && nav.languages[0]) { + return nav.languages[0]; + } + if (nav.language) { + return nav.language; + } + return nav.userLanguage; +} + +export function getLanguageWithoutRegionCode(nav) { + return getLanguage(nav) + .toLowerCase() + .split(/[_-]+/)[0]; +} diff --git a/awx/ui_next/src/util/language.test.jsx b/awx/ui_next/src/util/language.test.jsx new file mode 100644 index 0000000000..b6a64a302c --- /dev/null +++ b/awx/ui_next/src/util/language.test.jsx @@ -0,0 +1,39 @@ +import { getLanguage, getLanguageWithoutRegionCode } from './language'; + +describe('getLanguage', () => { + test('it returns the expected language code', () => { + expect(getLanguage({ languages: ['es-US'] })).toEqual('es-US'); + expect( + getLanguage({ + languages: ['es-US'], + language: 'fr-FR', + userLanguage: 'en-US', + }) + ).toEqual('es-US'); + expect(getLanguage({ language: 'fr-FR', userLanguage: 'en-US' })).toEqual( + 'fr-FR' + ); + expect(getLanguage({ userLanguage: 'en-US' })).toEqual('en-US'); + }); +}); + +describe('getLanguageWithoutRegionCode', () => { + test('it returns the expected (truncated) language code', () => { + expect(getLanguageWithoutRegionCode({ languages: ['es-US'] })).toEqual( + 'es' + ); + expect( + getLanguageWithoutRegionCode({ + languages: ['es-US'], + language: 'fr-FR', + userLanguage: 'en-US', + }) + ).toEqual('es'); + expect( + getLanguageWithoutRegionCode({ language: 'fr-FR', userLanguage: 'en-US' }) + ).toEqual('fr'); + expect(getLanguageWithoutRegionCode({ userLanguage: 'en-US' })).toEqual( + 'en' + ); + }); +}); From 9421781cc7acb46ceb16e673fd435af3ec7ddd31 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 3 Oct 2019 20:02:14 -0400 Subject: [PATCH 2/3] Add basic date formatter --- awx/ui_next/src/util/dates.jsx | 6 ++++++ awx/ui_next/src/util/dates.test.jsx | 13 +++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 awx/ui_next/src/util/dates.jsx create mode 100644 awx/ui_next/src/util/dates.test.jsx diff --git a/awx/ui_next/src/util/dates.jsx b/awx/ui_next/src/util/dates.jsx new file mode 100644 index 0000000000..644c896562 --- /dev/null +++ b/awx/ui_next/src/util/dates.jsx @@ -0,0 +1,6 @@ +/* eslint-disable import/prefer-default-export */ +import { getLanguage } from './language'; + +export function formatDateString(dateString, lang = getLanguage(navigator)) { + return new Date(dateString).toLocaleString(lang); +} diff --git a/awx/ui_next/src/util/dates.test.jsx b/awx/ui_next/src/util/dates.test.jsx new file mode 100644 index 0000000000..90b02c1185 --- /dev/null +++ b/awx/ui_next/src/util/dates.test.jsx @@ -0,0 +1,13 @@ +import { formatDateString } from './dates'; + +describe('formatDateString', () => { + test('it returns the expected value', () => { + const lang = 'en-US'; + expect(formatDateString('', lang)).toEqual('Invalid Date'); + expect(formatDateString({}, lang)).toEqual('Invalid Date'); + expect(formatDateString(undefined, lang)).toEqual('Invalid Date'); + expect(formatDateString('2018-01-31T01:14:52.969227Z', lang)).toEqual( + '1/31/2018, 1:14:52 AM' + ); + }); +}); From d3b0edf75a3239489b549cdf1ff2fff53b0a622a Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 3 Oct 2019 20:02:55 -0400 Subject: [PATCH 3/3] Apply date formatter to lists and details --- awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx | 13 ++++++++++--- .../src/screens/Job/JobDetail/JobDetail.test.jsx | 4 ++-- awx/ui_next/src/screens/Job/JobList/JobListItem.jsx | 5 ++++- .../OrganizationDetail/OrganizationDetail.jsx | 12 +++++++++--- .../OrganizationDetail/OrganizationDetail.test.jsx | 8 ++++---- .../JobTemplateDetail/JobTemplateDetail.jsx | 9 +++++++-- 6 files changed, 36 insertions(+), 15 deletions(-) diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx index 6763f7a3c1..b082a7a09e 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx @@ -13,7 +13,8 @@ import ErrorDetail from '@components/ErrorDetail'; import LaunchButton from '@components/LaunchButton'; import { StatusIcon } from '@components/Sparkline'; import { toTitleCase } from '@util/strings'; -import { Job } from '../../../types'; +import { formatDateString } from '@util/dates'; +import { Job } from '@types'; import { JobsAPI, ProjectUpdatesAPI, @@ -140,8 +141,14 @@ function JobDetail({ job, i18n, history }) { } /> - - + + {jobTemplate && ( ', () => { } assertDetail('Status', 'Successful'); - assertDetail('Started', mockJobData.started); - assertDetail('Finished', mockJobData.finished); + assertDetail('Started', '8/8/2019, 7:24:18 PM'); + assertDetail('Finished', '8/8/2019, 7:24:50 PM'); assertDetail('Template', mockJobData.summary_fields.job_template.name); assertDetail('Job Type', 'Run'); assertDetail('Launched By', mockJobData.summary_fields.created_by.username); diff --git a/awx/ui_next/src/screens/Job/JobList/JobListItem.jsx b/awx/ui_next/src/screens/Job/JobList/JobListItem.jsx index 495edc0d0b..33b3d0d373 100644 --- a/awx/ui_next/src/screens/Job/JobList/JobListItem.jsx +++ b/awx/ui_next/src/screens/Job/JobList/JobListItem.jsx @@ -16,6 +16,7 @@ import DataListCheck from '@components/DataListCheck'; import LaunchButton from '@components/LaunchButton'; import VerticalSeparator from '@components/VerticalSeparator'; import { toTitleCase } from '@util/strings'; +import { formatDateString } from '@util/dates'; import { JOB_TYPE_URL_SEGMENTS } from '../../../constants'; const StyledButton = styled(PFButton)` @@ -56,7 +57,9 @@ class JobListItem extends Component { , {toTitleCase(job.type)}, - {job.finished}, + + {formatDateString(job.finished)} + , {job.type !== 'system_job' && job.summary_fields.user_capabilities.start && ( diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx index 29fda4aa71..42594d5da9 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx @@ -10,6 +10,7 @@ import { DetailList, Detail } from '@components/DetailList'; import { ChipGroup, Chip } from '@components/Chip'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; +import { formatDateString } from '@util/dates'; const CardBody = styled(PFCardBody)` padding-top: 20px; @@ -53,7 +54,6 @@ class OrganizationDetail extends Component { render() { const { hasContentLoading, contentError, instanceGroups } = this.state; - const { organization: { name, @@ -86,8 +86,14 @@ class OrganizationDetail extends Component { label={i18n._(t`Ansible Environment`)} value={custom_virtualenv} /> - - + + {instanceGroups && instanceGroups.length > 0 && ( ', () => { description: 'Bar', custom_virtualenv: 'Fizz', max_hosts: '0', - created: 'Bat', - modified: 'Boo', + created: '2015-07-07T17:21:26.429745Z', + modified: '2019-08-11T19:47:37.980466Z', summary_fields: { user_capabilities: { edit: true, @@ -63,8 +63,8 @@ describe('', () => { { label: 'Name', value: 'Foo' }, { label: 'Description', value: 'Bar' }, { label: 'Ansible Environment', value: 'Fizz' }, - { label: 'Created', value: 'Bat' }, - { label: 'Last Modified', value: 'Boo' }, + { label: 'Created', value: '7/7/2015, 5:21:26 PM' }, + { label: 'Last Modified', value: '8/11/2019, 7:47:37 PM' }, { label: 'Max Hosts', value: '0' }, ]; // eslint-disable-next-line no-restricted-syntax diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx index 4515034a90..9fadf59be6 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx @@ -17,6 +17,7 @@ import LaunchButton from '@components/LaunchButton'; import ContentLoading from '@components/ContentLoading'; import { ChipGroup, Chip, CredentialChip } from '@components/Chip'; import { DetailList, Detail } from '@components/DetailList'; +import { formatDateString } from '@util/dates'; import { JobTemplatesAPI } from '@api'; const ButtonGroup = styled.div` @@ -168,11 +169,15 @@ class JobTemplateDetail extends Component {