From 8e3af1b91b1cc874d4e867717eef1dc9eca592fc Mon Sep 17 00:00:00 2001 From: Debsmita Santra Date: Fri, 31 May 2024 10:59:34 +0530 Subject: [PATCH] fix(scaffolder): update annotator action readme (#1638) --- plugins/scaffolder-annotator-action/README.md | 143 +++++++++++++----- .../templates/01-scaffolder-template.yaml | 17 ++- .../src/actions/annotator/annotator.test.ts | 33 ++-- .../src/actions/annotator/annotator.ts | 59 +++++--- .../src/dynamic/index.ts | 12 +- .../scaffolder-annotator-action/src/module.ts | 7 +- .../src/utils/convertLabelsToObject.ts | 31 ---- .../src/utils/getCurrentTimestamp.test.ts | 27 ---- .../src/utils/getCurrentTimestamp.ts | 14 +- 9 files changed, 196 insertions(+), 147 deletions(-) delete mode 100644 plugins/scaffolder-annotator-action/src/utils/convertLabelsToObject.ts delete mode 100644 plugins/scaffolder-annotator-action/src/utils/getCurrentTimestamp.test.ts diff --git a/plugins/scaffolder-annotator-action/README.md b/plugins/scaffolder-annotator-action/README.md index 4b86fb75d9..ae0dd36a67 100644 --- a/plugins/scaffolder-annotator-action/README.md +++ b/plugins/scaffolder-annotator-action/README.md @@ -2,11 +2,25 @@ The annotator module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend). -This module enables users to generate custom actions for annotating their entity object(s). +This module allows users to create custom actions for annotating their entity objects. Additionally, it enables users to utilize existing custom actions provided by the module for annotating entities with timestamps and scaffolder entity references. -## Getting started +## Installation -### Installing on the new backend system +### Available custom actions + +| Action | Description | +| ------------------------- | :-----------------------------------------------------------------------------------------------------: | +| `catalog:timestamping` | Adds the `backstage.io/createdAt` annotation containing the current timestamp to your entity object | +| `catalog:scaffolded-from` | Adds `scaffoldedFrom` spec containing the template entityRef to your entity object | +| `catalog:annotate` | Allows you to annotate your entity object with specified label(s), annotation(s) and spec property(ies) | + +To begin, install the module package into the backend workspace of your backstage instance: + +```console +yarn workspace backend add @janus-idp/backstage-scaffolder-backend-module-annotator +``` + +### Registering the annotator action plugin with the new backend system To install the module into the [new backend system](https://backstage.io/docs/backend-system/), add the following into the `packages/backend/src/index.ts` file: @@ -22,15 +36,9 @@ backend.add( backend.start(); ``` -### Installing on the legacy backend system +### Registering the annotator action plugin with the legacy backend system -1. Install the Annotator custom action plugin using the following command: - -```console -yarn workspace backend add @janus-idp/backstage-scaffolder-backend-module-annotator -``` - -2. Integrate the custom actions in the `scaffolder-backend` `createRouter` function: +1. Register the custom actions in the `packages/backend/src/plugins/scaffolder.ts` file: - Install the `@backstage/integration` package using the following command: @@ -38,7 +46,7 @@ yarn workspace backend add @janus-idp/backstage-scaffolder-backend-module-annota yarn workspace backend add @backstage/integration ``` -- Add the `createTimestampAction` and `createScaffoldedFromAction` actions with the other built-in actions: +- Add the `createTimestampAction`, `createScaffoldedFromAction` and `createAnnotatorAction` with the other built-in actions: ```ts title="packages/backend/src/plugins/scaffolder.ts" @@ -49,7 +57,7 @@ yarn workspace backend add @janus-idp/backstage-scaffolder-backend-module-annota createBuiltinActions, createRouter, } from '@backstage/plugin-scaffolder-backend'; - import { createTimestampAction, createScaffoldedFromAction } from '@janus-idp/backstage-scaffolder-backend-module-annotator'; + import { createTimestampAction, createScaffoldedFromAction, createAnnotatorAction } from '@janus-idp/backstage-scaffolder-backend-module-annotator'; /* highlight-add-end */ ... @@ -67,7 +75,7 @@ yarn workspace backend add @janus-idp/backstage-scaffolder-backend-module-annota config: env.config, reader: env.reader, }); - const actions = [...builtInActions, createTimestampAction(), createScaffoldedFromAction()]; + const actions = [...builtInActions, createTimestampAction(), createScaffoldedFromAction(), createAnnotatorAction()]; /* highlight-add-end */ @@ -79,7 +87,7 @@ yarn workspace backend add @janus-idp/backstage-scaffolder-backend-module-annota }); ``` -3. To annotate the catalog-info.yaml skeleton with the current timestamp, add the **catalog:timestamping** custom action in your template yaml after the `Fetch Skeleton + Template` step: +2. To annotate the `catalog-info.yaml` skeleton with the current timestamp, add the `catalog:timestamping` custom action in your template's YAML file after the `Fetch Skeleton + Template` step. Refer to the [example templates](./examples/) for examples on how it's used in a template. ```yaml title="template.yaml" steps: @@ -95,7 +103,7 @@ steps: # highlight-add-end ``` -4. To annotate the catalog-info.yaml skeleton with the template entityRef, add the **catalog:scaffolded-from** custom action in your template yaml after the `Fetch Skeleton + Template` step: +3. To annotate the `catalog-info.yaml` skeleton with the template's `entityRef`, add the `catalog:scaffolded-from` custom action in your YAML file after the `Fetch Skeleton + Template` step. Refer to the [example templates](./examples/) for examples on how it is used in a template. ```yaml "title=template.yaml" steps: @@ -111,30 +119,80 @@ steps: ``` -## Usage +4. To use the `catalog:annotate` action to annotate your entity object. Refer to the [example templates](./examples/) for examples on how it is used in a template. -To use the `createAnnotatorAction` to create a custom action +```yaml "title=template.yaml" +steps: + - id: template + name: Fetch Skeleton + Template + action: fetch:template + ... + # highlight-add-start + - id: add-fields-to-catalog-info + name: Add a few fields into `catalog-info.yaml` using the generic action + action: catalog:annotate + input: + labels: + custom: ${{ parameters.label }} + other: "test-label" + annotations: + custom.io/annotation: ${{ parameters.label }} + custom.io/other: "value" + # highlight-add-end + +``` + +## Creating custom actions for your templates using the annotator module + +### Create the custom action + +#### The `createAnnotatorAction` action accepts the following parameters: + +| Parameter Name | Type | Required | Description | +| ---------------------------------- | :------: | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `actionId` | `string` | Yes | A unique id for the action. Default: `catalog:annotate`, please provide one or else it may conflict with the generic `catalog:annotate` custom action that is provided by this module. | +| `actionDescription` | `string` | No | A description of what the action accomplishes. Default: "Creates a new scaffolder action to annotate the entity object with specified label(s), annotation(s) and spec property(ies)." | +| `loggerInfoMsg` | `string` | No | A message that will be logged upon the execution of the action. Default: "Annotating your object" | +| `annotateEntityObject.labels` | `object` | No | Key-value pairs to be added to the `metadata.labels` of the entity | +| `annotateEntityObject.annotations` | `object` | No | Key-value pairs to be added to the `metadata.annotations` of the entity | +| `annotateEntityObject.spec` | `object` | No | Key-value pairs to be added to the `spec` of the entity. The value for each key can either be a string or an object with the key `readFromContext`, enabling users to specify the path in the context from which the value should be retrieved. | + +1. Create your [custom action](https://backstage.io/docs/features/software-templates/writing-custom-actions#writing-your-custom-action) + +2. Add the annotator module package `@janus-idp/backstage-scaffolder-backend-module-annotator` into your module's `package.json` -| Parameter Name | Type | Required | Description | -| ---------------------------------- | :------: | :------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `actionId` | `string` | Yes | A unique id for the action | -| `actionDescription` | `string` | Yes | A description of what the action accomplishes | -| `loggerInfoMsg` | `string` | No | A message that will be logged upon the execution of the action | -| `annotateEntityObject.labels` | `object` | No | Key-value pairs to be added to the `metadata.labels` of the entity | -| `annotateEntityObject.annotations` | `object` | No | Key-value pairs to be added to the `metadata.annotations` of the entity | -| `annotateEntityObject.spec` | `object` | No | Key-value pairs to be added to the `metadata.spec` of the entity. The value for each key can either be a string or an object with the key `readFromContext`, enabling users to specify the path in the context from which the value should be retrieved. | +3. In the action file, add the following snippet to it: -The annotator action accepts the following inputs +```ts createAddCompanyTitleAction.ts +// highlight-add-start +import { createAnnotatorAction } from '@janus-idp/backstage-scaffolder-backend-module-annotator'; + +export const createAddCompanyTitleAction = () => { + return createAnnotatorAction( + 'catalog:company-title', + 'Creates a new `catalog:company-title` Scaffolder action to annotate scaffolded entities with the company title.', + 'Annotating catalog-info.yaml with the company title', + ); +}; +// highlight-add-end +``` + +4. Install the custom action into your backstage instance following steps similar to the [installation instructions above](#installation) + +### Use your custom action in your desired template(s) + +#### The annotator template action accepts the following inputs #### Input -| Parameter Name | Type | Required | Description | -| ---------------- | :------: | :------: | ----------------------------------------------------------------------------- | -| `labels` | `object` | No | Key-value pairs to be added to the `metadata.labels` of the entity | -| `annotations` | `object` | No | Key-value pairs to be added to the `metadata.annotations` of the entity | -| `entityFilePath` | `string` | No | The file path from which the YAML representation of the entity should be read | -| `objectYaml` | `object` | No | The YAML representation of the object/entity | -| `writeToFile` | `string` | No | The file path where the YAML representation of the entity should be stored | +| Parameter Name | Type | Required | Description | +| ---------------- | :------: | :------: | ------------------------------------------------------------------------------------------------------------------ | +| `labels` | `object` | No | Key-value pairs to be added to the `metadata.labels` of the entity | +| `annotations` | `object` | No | Key-value pairs to be added to the `metadata.annotations` of the entity | +| `spec` | `object` | No | Key-value pairs to be added to the `spec` of the entity | +| `entityFilePath` | `string` | No | The file path from which the YAML representation of the entity should be read | +| `objectYaml` | `object` | No | The YAML representation of the object/entity | +| `writeToFile` | `string` | No | The file path where the YAML representation of the entity should be stored. Default value is './catalog-info.yaml' | #### Output @@ -142,9 +200,16 @@ The annotator action accepts the following inputs | ----------------- | :------: | -------------------------------------------------------------------------------------------- | | `annotatedObject` | `object` | The entity object marked with your specified annotation(s), label(s), and spec property(ies) | -Here are the custom actions currently available: +To annotate the entity file, add your custom action to your template file after `Fetch Skeleton + Template` step. Please note that your custom action needs to be installed into the backstage instance running the software template. -| Action | Description | -| ------------------------- | :-------------------------------------------: | -| `catalog:timestamping` | Adds current timestamp to your entity object | -| `catalog:scaffolded-from` | Adds template entityRef to your entity object | +```yaml +// highlight-add-start +- id: company-title + name: Add company title to catalog-info.yaml + action: catalog:company-title + input: + labels: { + company: 'My Company' + } +// highlight-add-end +``` diff --git a/plugins/scaffolder-annotator-action/examples/templates/01-scaffolder-template.yaml b/plugins/scaffolder-annotator-action/examples/templates/01-scaffolder-template.yaml index 114399e17a..ac8b15cac1 100644 --- a/plugins/scaffolder-annotator-action/examples/templates/01-scaffolder-template.yaml +++ b/plugins/scaffolder-annotator-action/examples/templates/01-scaffolder-template.yaml @@ -57,11 +57,25 @@ spec: description: ${{ parameters.description }} destination: ${{ parameters.repoUrl | parseRepoUrl }} owner: ${{ parameters.owner }} - + + # this step is an example of using the `catalog:timestamping` action - id: timestamp name: Add Timestamp to catalog-info.yaml action: catalog:timestamping + + # this step is an example of using the `catalog:annotate` action + - id: add-fields-to-catalog-info + name: Add a few fields into `catalog-info.yaml` using the generic action + action: catalog:annotate + input: + labels: + custom: ${{ parameters.label }} + other: "test-label" + annotations: + custom.io/annotation: ${{ parameters.label }} + custom.io/other: "value" + # this step is an example of using the `catalog:scaffolded-from` action - id: append-templateRef name: Append the entityRef of this template to the entityRef action: catalog:scaffolded-from @@ -74,6 +88,7 @@ spec: - github.com description: This is ${{ parameters.component_id }} repoUrl: ${{ parameters.repoUrl }} + repoVisibility: public - id: register name: Register diff --git a/plugins/scaffolder-annotator-action/src/actions/annotator/annotator.test.ts b/plugins/scaffolder-annotator-action/src/actions/annotator/annotator.test.ts index 6f427d3c8b..ab51e21231 100644 --- a/plugins/scaffolder-annotator-action/src/actions/annotator/annotator.test.ts +++ b/plugins/scaffolder-annotator-action/src/actions/annotator/annotator.test.ts @@ -5,7 +5,6 @@ import * as yaml from 'yaml'; import { PassThrough } from 'stream'; -import { convertLabelsToObject } from '../../utils/convertLabelsToObject'; import { getCurrentTimestamp } from '../../utils/getCurrentTimestamp'; import { createAnnotatorAction } from './annotator'; @@ -79,8 +78,16 @@ describe('catalog annotator', () => { logger, logStream: new PassThrough(), input: { - labels: 'label1=value1;label2=value2;label3=value3', - annotations: 'annotation1=value1;annotation2=value2;annotation3=value3', + labels: { + label1: 'value1', + label2: 'value2', + label3: 'value3', + }, + annotations: { + annotation1: 'value1', + annotation2: 'value2', + annotation3: 'value3', + }, }, output: jest.fn(), createTemporaryDirectory() { @@ -98,11 +105,11 @@ describe('catalog annotator', () => { ...entity.metadata, labels: { ...entity.metadata.labels, - ...convertLabelsToObject(mockContext.input.labels), + ...mockContext.input.labels, }, annotations: { ...entity.metadata.annotations, - ...convertLabelsToObject(mockContext.input.annotations), + ...mockContext.input.annotations, }, }, }; @@ -144,8 +151,16 @@ describe('catalog annotator', () => { logger, logStream: new PassThrough(), input: { - labels: 'label1=value1;label2=value2;label3=value3', - annotations: 'annotation1=value1;annotation2=value2;annotation3=value3', + labels: { + label1: 'value1', + label2: 'value2', + label3: 'value3', + }, + annotations: { + annotation1: 'value1', + annotation2: 'value2', + annotation3: 'value3', + }, objectYaml: obj, }, output: jest.fn(), @@ -163,11 +178,11 @@ describe('catalog annotator', () => { ...obj.metadata, labels: { ...(obj.metadata.labels || {}), - ...convertLabelsToObject(mockContext.input.labels), + ...mockContext.input.labels, }, annotations: { ...(obj.metadata.annotations || {}), - ...convertLabelsToObject(mockContext.input.annotations), + ...mockContext.input.annotations, }, }, }; diff --git a/plugins/scaffolder-annotator-action/src/actions/annotator/annotator.ts b/plugins/scaffolder-annotator-action/src/actions/annotator/annotator.ts index e3792648e8..dc06c79791 100644 --- a/plugins/scaffolder-annotator-action/src/actions/annotator/annotator.ts +++ b/plugins/scaffolder-annotator-action/src/actions/annotator/annotator.ts @@ -4,7 +4,6 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import * as fs from 'fs-extra'; import * as yaml from 'yaml'; -import { convertLabelsToObject } from '../../utils/convertLabelsToObject'; import { getObjectToAnnotate } from '../../utils/getObjectToAnnotate'; import { resolveSpec, Value } from '../../utils/resolveSpec'; @@ -14,8 +13,8 @@ import { resolveSpec, Value } from '../../utils/resolveSpec'; */ export const createAnnotatorAction = ( - actionId: string, - actionDescription: string, + actionId: string = 'catalog:annotate', + actionDescription?: string, loggerInfoMsg?: string, annotateEntityObject?: { annotations?: { [key: string]: string }; @@ -24,27 +23,38 @@ export const createAnnotatorAction = ( }, ) => { return createTemplateAction<{ - labels?: string; - annotations?: string; + labels?: { [key: string]: string }; + annotations?: { [key: string]: string }; + spec?: { [key: string]: string }; entityFilePath?: string; - objectYaml?: object; + objectYaml?: { [key: string]: string }; writeToFile?: string; }>({ id: actionId, - description: actionDescription, + description: + actionDescription || + 'Creates a new scaffolder action to annotate the entity object with specified label(s), annotation(s) and spec property(ies).', schema: { input: { type: 'object', properties: { labels: { title: 'Labels', - description: 'Labels that will be applied to the object', - type: 'string', + description: + 'Labels that will be applied to the `metadata.labels` of the entity object', + type: 'object', }, annotations: { title: 'Annotations', - description: 'Annotations that will be applied to the object', - type: 'string', + description: + 'Annotations that will be applied to the `metadata.annotations` of the entity object', + type: 'object', + }, + spec: { + title: 'Spec', + description: + 'Key-Value pair(s) that will be applied to the `spec` of the entity object', + type: 'object', }, entityFilePath: { title: 'Entity File Path', @@ -58,7 +68,8 @@ export const createAnnotatorAction = ( }, writeToFile: { title: 'Write To File', - description: 'Path to the file you want to write', + description: + 'Path to the file you want to write. Default path `./catalog-info.yaml`', type: 'string', }, }, @@ -92,45 +103,45 @@ export const createAnnotatorAction = ( annotations: { ...(objToAnnotate.metadata.annotations || {}), ...(annotateEntityObject?.annotations || {}), - ...convertLabelsToObject(ctx.input?.annotations), + ...(ctx.input?.annotations || {}), }, labels: { ...(objToAnnotate.metadata.labels || {}), ...(annotateEntityObject?.labels || {}), - ...convertLabelsToObject(ctx.input?.labels), + ...(ctx.input?.labels || {}), }, }, spec: { ...(objToAnnotate.spec || {}), ...resolveSpec(annotateEntityObject?.spec, ctx), + ...(ctx.input?.spec || {}), }, }; const result = yaml.stringify(annotatedObj); - ctx.logger.info(loggerInfoMsg || 'Annotating your object'); if ( - Object.keys(annotateEntityObject || {}).length > 0 && Object.keys( annotateEntityObject?.labels || annotateEntityObject?.annotations || annotateEntityObject?.spec || + ctx.input?.labels || + ctx.input?.annotations || + ctx.input?.spec || {}, ).length > 0 ) { - await fs.writeFile( - resolveSafeChildPath(ctx.workspacePath, './catalog-info.yaml'), - result, - 'utf8', - ); - } + ctx.logger.info(loggerInfoMsg || 'Annotating your object'); - if (ctx.input?.writeToFile) { await fs.writeFile( - resolveSafeChildPath(ctx.workspacePath, ctx.input.writeToFile), + resolveSafeChildPath( + ctx.workspacePath, + ctx.input?.writeToFile || './catalog-info.yaml', + ), result, 'utf8', ); } + ctx.output('annotatedObject', result); }, }); diff --git a/plugins/scaffolder-annotator-action/src/dynamic/index.ts b/plugins/scaffolder-annotator-action/src/dynamic/index.ts index 62a3047ffe..315702c2c1 100644 --- a/plugins/scaffolder-annotator-action/src/dynamic/index.ts +++ b/plugins/scaffolder-annotator-action/src/dynamic/index.ts @@ -1,8 +1,16 @@ import { BackendDynamicPluginInstaller } from '@backstage/backend-dynamic-feature-service'; -import { createScaffoldedFromAction, createTimestampAction } from '../actions'; +import { + createAnnotatorAction, + createScaffoldedFromAction, + createTimestampAction, +} from '../actions'; export const dynamicPluginInstaller: BackendDynamicPluginInstaller = { kind: 'legacy', - scaffolder: () => [createTimestampAction(), createScaffoldedFromAction()], + scaffolder: () => [ + createTimestampAction(), + createScaffoldedFromAction(), + createAnnotatorAction(), + ], }; diff --git a/plugins/scaffolder-annotator-action/src/module.ts b/plugins/scaffolder-annotator-action/src/module.ts index 80e161a0ce..1bd1e2c0db 100644 --- a/plugins/scaffolder-annotator-action/src/module.ts +++ b/plugins/scaffolder-annotator-action/src/module.ts @@ -1,7 +1,11 @@ import { createBackendModule } from '@backstage/backend-plugin-api'; import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; -import { createScaffoldedFromAction, createTimestampAction } from './actions'; +import { + createAnnotatorAction, + createScaffoldedFromAction, + createTimestampAction, +} from './actions'; /***/ /** @@ -20,6 +24,7 @@ export const scaffolderCustomActionsScaffolderModule = createBackendModule({ async init({ scaffolder }) { scaffolder.addActions(createScaffoldedFromAction()); scaffolder.addActions(createTimestampAction()); + scaffolder.addActions(createAnnotatorAction()); }, }); }, diff --git a/plugins/scaffolder-annotator-action/src/utils/convertLabelsToObject.ts b/plugins/scaffolder-annotator-action/src/utils/convertLabelsToObject.ts deleted file mode 100644 index d388a0fd7c..0000000000 --- a/plugins/scaffolder-annotator-action/src/utils/convertLabelsToObject.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const convertLabelsToObject = ( - labelsString: string | undefined, -): { [key: string]: string } => { - const result: { [key: string]: string } = {}; - - if (!labelsString || labelsString.indexOf('=') === -1) { - console.error( - "Invalid label string. Label string must contain at least one label separated by '=' character.", - ); - return result; - } - - const labelsArray = labelsString.split(';'); - - labelsArray.forEach(label => { - const separatorIndex = label.indexOf('='); - if (separatorIndex !== -1) { - const key = label.slice(0, separatorIndex).trim(); - const value = label.slice(separatorIndex + 1).trim(); - if (key && value) { - result[key] = value; - } - } else { - console.error( - `Invalid label: '${label}'. Label must contain at least one '=' character.`, - ); - } - }); - - return result; -}; diff --git a/plugins/scaffolder-annotator-action/src/utils/getCurrentTimestamp.test.ts b/plugins/scaffolder-annotator-action/src/utils/getCurrentTimestamp.test.ts deleted file mode 100644 index 8be5bb959a..0000000000 --- a/plugins/scaffolder-annotator-action/src/utils/getCurrentTimestamp.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getCurrentTimestamp } from './getCurrentTimestamp'; - -describe('getCurrentTimestamp', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should return date and time', async () => { - const dateObj = new Date(); - dateObj.setDate(12); - dateObj.setMonth(4); - dateObj.setFullYear(2021); - dateObj.setHours(7); - dateObj.setMinutes(3); - dateObj.setSeconds(18); - let dateAndTime = getCurrentTimestamp(dateObj); - expect(dateAndTime).toBe('5/12/2021, 07:03:18 AM'); - - dateObj.setHours(14); - dateAndTime = getCurrentTimestamp(dateObj); - expect(dateAndTime).toBe('5/12/2021, 02:03:18 PM'); - - dateObj.setMinutes(20); - dateAndTime = getCurrentTimestamp(dateObj); - expect(dateAndTime).toBe('5/12/2021, 02:20:18 PM'); - }); -}); diff --git a/plugins/scaffolder-annotator-action/src/utils/getCurrentTimestamp.ts b/plugins/scaffolder-annotator-action/src/utils/getCurrentTimestamp.ts index 5c909ed09b..7e7c717de1 100644 --- a/plugins/scaffolder-annotator-action/src/utils/getCurrentTimestamp.ts +++ b/plugins/scaffolder-annotator-action/src/utils/getCurrentTimestamp.ts @@ -1,16 +1,4 @@ -const zeroPad = (d: number) => - d < 10 ? '0'.concat(d.toString()) : d.toString(); - export const getCurrentTimestamp = (date?: Date) => { const dateObj = date || new Date(Date.now()); - - const time = - dateObj.getHours() > 12 - ? `${zeroPad(dateObj.getHours() - 12)}:${zeroPad( - dateObj.getMinutes(), - )}:${zeroPad(dateObj.getSeconds())} PM` - : `${zeroPad(dateObj.getHours())}:${zeroPad( - dateObj.getMinutes(), - )}:${zeroPad(dateObj.getSeconds())} AM`; - return `${dateObj.toLocaleDateString()}, ${time}`; + return `${dateObj.toLocaleDateString()}, ${dateObj.toLocaleTimeString('en-US')}`; };