diff --git a/workspaces/homepage/.changeset/gold-ways-hear.md b/workspaces/homepage/.changeset/gold-ways-hear.md
new file mode 100644
index 000000000..3419a2d0d
--- /dev/null
+++ b/workspaces/homepage/.changeset/gold-ways-hear.md
@@ -0,0 +1,7 @@
+---
+'@red-hat-developer-hub/backstage-plugin-dynamic-home-page': minor
+---
+
+- Added support to show also the the username (`displayName` from the user catalog entity) in the header title.
+- Added additional options to show the local time and a worldclock to the header.
+- Added a new `WorldClock` card based on the Home plugin `HeaderWorldClock` component to show additional clocks/timezones also in the home page content area.
diff --git a/workspaces/homepage/.prettierignore b/workspaces/homepage/.prettierignore
index 9583639ad..d15208cbf 100644
--- a/workspaces/homepage/.prettierignore
+++ b/workspaces/homepage/.prettierignore
@@ -4,3 +4,5 @@ coverage
.vscode
.eslintrc.js
report.api.md
+knip-report.md
+
diff --git a/workspaces/homepage/app-config.yaml b/workspaces/homepage/app-config.yaml
index ca52ec530..46e61bfde 100644
--- a/workspaces/homepage/app-config.yaml
+++ b/workspaces/homepage/app-config.yaml
@@ -80,15 +80,9 @@ catalog:
locations:
# Local example data, file locations are relative to the backend process, typically `packages/backend`
- type: file
- target: ../../examples/entities.yaml
-
- # Local example template
+ target: ../../catalog-info.yaml
- type: file
- target: ../../examples/template/template.yaml
- rules:
- - allow: [Template]
-
- # Local example organizational data
+ target: ../../examples/entities.yaml
- type: file
target: ../../examples/org.yaml
rules:
diff --git a/workspaces/homepage/docs/customization.md b/workspaces/homepage/docs/defaults.md
similarity index 99%
rename from workspaces/homepage/docs/customization.md
rename to workspaces/homepage/docs/defaults.md
index daa514a89..818834ee6 100644
--- a/workspaces/homepage/docs/customization.md
+++ b/workspaces/homepage/docs/defaults.md
@@ -1,4 +1,4 @@
-# Customization
+# Defaults
The dynamic home page allows admins to customize the homepage in the `app-config`, and plugin authors to extend the home page with additional cards or content.
diff --git a/workspaces/homepage/docs/header-customize-subtitle.png b/workspaces/homepage/docs/header-customize-subtitle.png
new file mode 100644
index 000000000..d26b69ede
Binary files /dev/null and b/workspaces/homepage/docs/header-customize-subtitle.png differ
diff --git a/workspaces/homepage/docs/header-customize-title.png b/workspaces/homepage/docs/header-customize-title.png
new file mode 100644
index 000000000..0b1bdcf13
Binary files /dev/null and b/workspaces/homepage/docs/header-customize-title.png differ
diff --git a/workspaces/homepage/docs/header-default-1.0.png b/workspaces/homepage/docs/header-default-1.0.png
new file mode 100644
index 000000000..dde201127
Binary files /dev/null and b/workspaces/homepage/docs/header-default-1.0.png differ
diff --git a/workspaces/homepage/docs/header-default-prepared.png b/workspaces/homepage/docs/header-default-prepared.png
new file mode 100644
index 000000000..3c74e7032
Binary files /dev/null and b/workspaces/homepage/docs/header-default-prepared.png differ
diff --git a/workspaces/homepage/docs/header-localtime-date.png b/workspaces/homepage/docs/header-localtime-date.png
new file mode 100644
index 000000000..e720ffad8
Binary files /dev/null and b/workspaces/homepage/docs/header-localtime-date.png differ
diff --git a/workspaces/homepage/docs/header-localtime-full-de.png b/workspaces/homepage/docs/header-localtime-full-de.png
new file mode 100644
index 000000000..07c2786cb
Binary files /dev/null and b/workspaces/homepage/docs/header-localtime-full-de.png differ
diff --git a/workspaces/homepage/docs/header-localtime-local-label.png b/workspaces/homepage/docs/header-localtime-local-label.png
new file mode 100644
index 000000000..a14c428e0
Binary files /dev/null and b/workspaces/homepage/docs/header-localtime-local-label.png differ
diff --git a/workspaces/homepage/docs/header-localtime-time.png b/workspaces/homepage/docs/header-localtime-time.png
new file mode 100644
index 000000000..d75ab1d75
Binary files /dev/null and b/workspaces/homepage/docs/header-localtime-time.png differ
diff --git a/workspaces/homepage/docs/header-personalized-title-with-displayname.png b/workspaces/homepage/docs/header-personalized-title-with-displayname.png
new file mode 100644
index 000000000..481a3cc08
Binary files /dev/null and b/workspaces/homepage/docs/header-personalized-title-with-displayname.png differ
diff --git a/workspaces/homepage/docs/header-personalized-title-without-displayname.png b/workspaces/homepage/docs/header-personalized-title-without-displayname.png
new file mode 100644
index 000000000..10c4db9c5
Binary files /dev/null and b/workspaces/homepage/docs/header-personalized-title-without-displayname.png differ
diff --git a/workspaces/homepage/docs/header-with-custom-pagetitle.png b/workspaces/homepage/docs/header-with-custom-pagetitle.png
new file mode 100644
index 000000000..a949d3b86
Binary files /dev/null and b/workspaces/homepage/docs/header-with-custom-pagetitle.png differ
diff --git a/workspaces/homepage/docs/header-worldclock.png b/workspaces/homepage/docs/header-worldclock.png
new file mode 100644
index 000000000..ea976cc40
Binary files /dev/null and b/workspaces/homepage/docs/header-worldclock.png differ
diff --git a/workspaces/homepage/docs/header.md b/workspaces/homepage/docs/header.md
new file mode 100644
index 000000000..3a54b24b8
--- /dev/null
+++ b/workspaces/homepage/docs/header.md
@@ -0,0 +1,269 @@
+# Header
+
+
+
+## Title
+
+The header title shows by default the message "Welcome back!".
+
+![Default header](header-default-1.0.png)
+
+
+
+The title can be changed by overriding the `title` property of the dynamic home page plugin:
+
+```yaml
+dynamicPlugins:
+ frontend:
+ red-hat-developer-hub.backstage-plugin-dynamic-home-page:
+ dynamicRoutes:
+ - path: /
+ importName: DynamicHomePage
+ config:
+ props:
+ title: 'Howdy {{firstName}} or {{dispayName}}'
+```
+
+The example will show:
+
+![Header with customized title](header-customize-title.png)
+
+The `title` property support currently two variables:
+
+- `{{displayName}}` contains the full `displayName` of the user catalog entity.
+- `{{firstName}}` contains the first part (seperated by a space) of the users `displayName`.
+
+There is currently no option to define different titles per hour.
+
+### Subtitle
+
+You can also use a `subtitle` property which isn't used by default:
+
+```yaml
+dynamicPlugins:
+ frontend:
+ red-hat-developer-hub.backstage-plugin-dynamic-home-page:
+ dynamicRoutes:
+ - path: /
+ importName: DynamicHomePage
+ config:
+ props:
+ title: Our custom RHDH instance
+ subtitle: 'Hello {{dispayName}}'
+```
+
+![Header with customized subtitle](header-customize-subtitle.png)
+
+The `subtitle` supports the same two variables as the `title`.
+
+### Personalized title
+
+Some titles might look just good if the users have or have not a profile `displayName` in their catalog `User` entity. To avoid an unnecessary space in "Welcome to your RHDH instance {{firstName}}!" when the firstname is not available, admins can configure the title separately for the case that the `displayName` is available (`personalizedTitle`) or not (`title`). `title` is always used as a fallback if `personalizedTitle` isn't configured.
+
+```yaml
+dynamicPlugins:
+ frontend:
+ red-hat-developer-hub.backstage-plugin-dynamic-home-page:
+ dynamicRoutes:
+ - path: /
+ importName: DynamicHomePage
+ config:
+ props:
+ title: Welcome to your RHDH instance!
+ personalizedTitle: Welcome to your RHDH instance {{firstName}}!
+```
+
+For users without a `displayName`:
+
+![Header with personalized title without display name](header-personalized-title-without-displayname.png)
+
+For users with a `displayName`:
+
+![Header with personalized title with display name](header-personalized-title-with-displayname.png)
+
+### Page title
+
+The page title could override the header title to display a slightly different text in the browser tab.
+
+```yaml
+dynamicPlugins:
+ frontend:
+ red-hat-developer-hub.backstage-plugin-dynamic-home-page:
+ dynamicRoutes:
+ - path: /
+ importName: DynamicHomePage
+ config:
+ props:
+ title: Welcome back!
+ pageTitle: Our Company
+```
+
+![Header with custom page title](header-with-custom-pagetitle.png)
+
+### Available props
+
+| Prop | Default | Description |
+| ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `title` | none | Change the header title, can contain `{{displayName}}` or `{{firstName}}` to include the current user. This requires a `displayName` in the user catalog entity. |
+| `subtitle` | none | The smaller subtitle supports the same variables as the title. |
+| `personalizedTitle` | none | An optional property to override the `title` if you like to differentiate users with and without a `displayName`. See example above. |
+| `pageTitle` | none | An optional property to override the title that is displayed in the browser tab. |
+
+## Local clock
+
+Starting with Dynamic Home Page plugin 1.1 (RHDH 1.5) the home page header can show the current time in the header.
+
+The local time can be disabled or configured with a `localClock` object. This object supports three properties: `label`, `format`, and `lang`. All of them are optional.
+
+To show the current time you must specific at least the `format`:
+
+```yaml
+dynamicPlugins:
+ frontend:
+ red-hat-developer-hub.backstage-plugin-dynamic-home-page:
+ dynamicRoutes:
+ - path: /
+ importName: DynamicHomePage
+ config:
+ props:
+ localClock:
+ format: time
+```
+
+![Header with local time](header-localtime-time.png)
+
+Showing the current date instead of time:
+
+```yaml
+dynamicPlugins:
+ frontend:
+ red-hat-developer-hub.backstage-plugin-dynamic-home-page:
+ dynamicRoutes:
+ - path: /
+ importName: DynamicHomePage
+ config:
+ props:
+ localClock:
+ format: date
+```
+
+![Header with local date](header-localtime-date.png)
+
+You can pick up more options like `both`, `full`, etc. The full list is available below.
+
+By default the format is based on the browser language settings. You can enforce a language setting with `lang`:
+
+```yaml
+dynamicPlugins:
+ frontend:
+ red-hat-developer-hub.backstage-plugin-dynamic-home-page:
+ dynamicRoutes:
+ - path: /
+ importName: DynamicHomePage
+ config:
+ props:
+ localClock:
+ format: full
+ lang: de
+```
+
+![Header with fill date and time](header-localtime-full-de.png)
+
+There is also an option to specify a label shown above the time. This is especially useful if you like to show additional times (see the world clock section below).
+
+```yaml
+dynamicPlugins:
+ frontend:
+ red-hat-developer-hub.backstage-plugin-dynamic-home-page:
+ dynamicRoutes:
+ - path: /
+ importName: DynamicHomePage
+ config:
+ props:
+ localClock:
+ format: time
+ label: Local
+```
+
+![Header with local label and time](header-localtime-local-label.png)
+
+### Available props
+
+| Prop | Default | Description |
+| ------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `localClock.format` | `none` | One of the options `none`, `time`, `timewithseconds`, `date`, `datewithweekday`, `both`, or `full` is currently supported. Other options will show the `time`. |
+| `localClock.lang` | none | The date and time format depends by default on the browser settings. The `lang` can enforce the same output format for all users. |
+| `localClock.label` | none | Optional label that is shown on top of the local time. |
+
+`localClock.format`:
+
+| Format | Example, the output depends on the browser language |
+| ----------------- | --------------------------------------------------- |
+| `none` | - |
+| `time` | 01:14 PM |
+| `timewithseconds` | 01:14:15 PM |
+| `date` | 01/01/2025 |
+| `datewithweekday` | Wednesday, 01/01/2025 |
+| `both` | 01/01/2025, 01:14 PM |
+| `full` | Wednesday, 01/01/2025, 01:14 PM |
+
+## World clock
+
+Also added with Dynamic Home Page plugin 1.1 (RHDH 1.5) the header can include additional clocks/timezones.
+
+The world clock option (thanks to the upstream home plugin) provides you the option to show multiple timezones on the home page header.
+
+```yaml
+dynamicPlugins:
+ frontend:
+ red-hat-developer-hub.backstage-plugin-dynamic-home-page:
+ dynamicRoutes:
+ - path: /
+ importName: DynamicHomePage
+ config:
+ props:
+ worldClocks:
+ - label: Raleigh
+ timeZone: EST
+ - label: London
+ timeZone: GMT
+ - label: Brno
+ timeZone: CET
+ - label: Bangalore
+ timeZone: IST
+```
+
+![Header with local time and world click](header-worldclock.png)
+
+### Available props
+
+| Prop | Default | Description |
+| ------------- | ------- | ------------------------------------------------------------------------------------------------------ |
+| `worldClocks` | none | The world clocks must be an array with two properties: `label` and `timeZone` as shown in the example. |
diff --git a/workspaces/homepage/docs/recently-visited.md b/workspaces/homepage/docs/recently-visited.md
index 48466a720..600eaa042 100644
--- a/workspaces/homepage/docs/recently-visited.md
+++ b/workspaces/homepage/docs/recently-visited.md
@@ -1,8 +1,7 @@
# Recently visited
> [!CAUTION]
-> This feature is not part of RHDH 1.3, it is planned for RHDH 1.4.
-> Follow [RHIDP-4235](https://issues.redhat.com/browse/RHIDP-4235) for more information.
+> This feature is not part of RHDH 1.3 and 1.4, it is planned for RHDH 1.5.
Shows the recently visited pages (incl. catalog entities) the current user visited.
diff --git a/workspaces/homepage/docs/top-visited.md b/workspaces/homepage/docs/top-visited.md
index c667fda73..2b07a590b 100644
--- a/workspaces/homepage/docs/top-visited.md
+++ b/workspaces/homepage/docs/top-visited.md
@@ -1,8 +1,7 @@
# Top visited
> [!CAUTION]
-> This feature is not part of RHDH 1.3, it is planned for RHDH 1.4.
-> Follow [RHIDP-4235](https://issues.redhat.com/browse/RHIDP-4235) for more information.
+> This feature is not part of RHDH 1.3 and 1.4, it is planned for RHDH 1.5.
Shows the top visited pages (incl. catalog entities) the current user visited.
diff --git a/workspaces/homepage/docs/worldclock.md b/workspaces/homepage/docs/worldclock.md
new file mode 100644
index 000000000..6f8763eaa
--- /dev/null
+++ b/workspaces/homepage/docs/worldclock.md
@@ -0,0 +1,45 @@
+# World clock
+
+> [!NOTE]
+> This feature is added Dynamic Home Page plugin 1.1 (RHDH 1.5).
+
+The world clock component (thanks again to the upstream Home plugin) is a great way show show multiple timezones on the home page!
+
+![Home page with world clock](worldclock.png)
+
+## Examples
+
+```yaml
+dynamicPlugins:
+ frontend:
+ red-hat-developer-hub.backstage-plugin-dynamic-home-page:
+ mountPoints:
+ - mountPoint: home.page/cards
+ importName: WorldClock
+ config:
+ layouts:
+ xl: { w: 12, h: 1 }
+ lg: { w: 12, h: 1 }
+ md: { w: 12, h: 1 }
+ sm: { w: 12, h: 1 }
+ xs: { w: 12, h: 1 }
+ xxs: { w: 12, h: 1 }
+ props:
+ worldClocks:
+ - label: Raleigh
+ timeZone: EST
+ - label: London
+ timeZone: GMT
+ - label: Brno
+ timeZone: CET
+ - label: Bangalore
+ timeZone: IST
+```
+
+## Available props
+
+| Prop | Default | Description |
+| ---------------- | --------------- | -------------------------------------------------------------------------------------------------------------- |
+| `worldClocks` | none | The world clocks must be an array with two properties: `label` and `timeZone` as shown in the example. |
+| `timeFormat` | none | An object to show additional parts of the date. Use `day: 2-digit` and `month: 2-digit` to show also the date. |
+| `justifyContent` | `space-between` | A CSS option to add spacing around the clocks (`space-around`) or just between the clocks (`space-between`). |
diff --git a/workspaces/homepage/docs/worldclock.png b/workspaces/homepage/docs/worldclock.png
new file mode 100644
index 000000000..8045a3614
Binary files /dev/null and b/workspaces/homepage/docs/worldclock.png differ
diff --git a/workspaces/homepage/examples/org.yaml b/workspaces/homepage/examples/org.yaml
index a10e81fc7..aedb0a876 100644
--- a/workspaces/homepage/examples/org.yaml
+++ b/workspaces/homepage/examples/org.yaml
@@ -3,8 +3,11 @@
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
+ namespace: development
name: guest
spec:
+ profile:
+ displayName: Guest
memberOf: [guests]
---
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group
diff --git a/workspaces/homepage/mkdocs.yaml b/workspaces/homepage/mkdocs.yaml
index 5730fb37c..0ef65b103 100644
--- a/workspaces/homepage/mkdocs.yaml
+++ b/workspaces/homepage/mkdocs.yaml
@@ -4,9 +4,12 @@ plugins:
- techdocs-core
nav:
- - Customization: customization.md
- - Layout options: layout-options.md
- - Cards:
+ - About: index.md
+ - Customization:
+ - Defaults: defaults.md
+ - Header: header.md
+ - Layout options: layout-options.md
+ - Included cards:
- Search: search.md
- Quick access: quick-access.md
- Headline: headline.md
@@ -17,5 +20,6 @@ nav:
- Top visited: top-visited.md
- Featured docs: featured-docs.md
- Jokes: jokes.md
- - Plugins:
+ - World clock: worldclock.md
+ - Extend with plugins:
- Create a new card: create-a-new-card.md
diff --git a/workspaces/homepage/package.json b/workspaces/homepage/package.json
index 070b1ed00..c541520a2 100644
--- a/workspaces/homepage/package.json
+++ b/workspaces/homepage/package.json
@@ -24,6 +24,7 @@
"lint:all": "backstage-cli repo lint",
"test:e2e": "playwright test",
"prettier:check": "prettier --check .",
+ "prettier:fix": "prettier --write .",
"new": "backstage-cli new --scope @red-hat-developer-hub",
"postinstall": "cd ../../ && yarn install"
},
diff --git a/workspaces/homepage/packages/app/knip-report.md b/workspaces/homepage/packages/app/knip-report.md
index f7181d63f..cb841156e 100644
--- a/workspaces/homepage/packages/app/knip-report.md
+++ b/workspaces/homepage/packages/app/knip-report.md
@@ -15,3 +15,4 @@
| @backstage/test-utils | package.json | error |
| @testing-library/dom | package.json | error |
| cross-env | package.json | error |
+
diff --git a/workspaces/homepage/packages/app/src/App.tsx b/workspaces/homepage/packages/app/src/App.tsx
index 4e6d071bf..3a2ee9ce3 100644
--- a/workspaces/homepage/packages/app/src/App.tsx
+++ b/workspaces/homepage/packages/app/src/App.tsx
@@ -44,6 +44,7 @@ import { Root } from './components/Root';
import {
AlertDisplay,
+ IdentityProviders,
OAuthRequestDialog,
SignInPage,
} from '@backstage/core-components';
@@ -52,12 +53,23 @@ import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
+import { githubAuthApiRef } from '@backstage/core-plugin-api';
import {
DynamicHomePage,
VisitListener,
} from '@red-hat-developer-hub/backstage-plugin-dynamic-home-page';
+const identityProviders: IdentityProviders = [
+ 'guest',
+ {
+ id: 'github-auth-provider',
+ title: 'GitHub',
+ message: 'Sign in using GitHub',
+ apiRef: githubAuthApiRef,
+ },
+];
+
const app = createApp({
apis,
bindRoutes({ bind }) {
@@ -78,7 +90,9 @@ const app = createApp({
});
},
components: {
- SignInPage: props => ,
+ SignInPage: props => (
+
+ ),
},
});
diff --git a/workspaces/homepage/packages/backend/knip-report.md b/workspaces/homepage/packages/backend/knip-report.md
index 607885deb..069ce6c31 100644
--- a/workspaces/homepage/packages/backend/knip-report.md
+++ b/workspaces/homepage/packages/backend/knip-report.md
@@ -1,16 +1,16 @@
# Knip report
-## Unused dependencies (10)
+## Unused dependencies (9)
+
+| Name | Location | Severity |
+| :------------------------------------ | :----------- | :------- |
+| @backstage/plugin-search-backend-node | package.json | error |
+| @backstage/plugin-permission-common | package.json | error |
+| @backstage/plugin-permission-node | package.json | error |
+| @backstage/plugin-auth-node | package.json | error |
+| @backstage/config | package.json | error |
+| better-sqlite3 | package.json | error |
+| node-gyp | package.json | error |
+| app | package.json | error |
+| pg | package.json | error |
-| Name | Location | Severity |
-| :---------------------------------------------------- | :----------- | :------- |
-| @backstage/plugin-auth-backend-module-github-provider | package.json | error |
-| @backstage/plugin-search-backend-node | package.json | error |
-| @backstage/plugin-permission-common | package.json | error |
-| @backstage/plugin-permission-node | package.json | error |
-| @backstage/plugin-auth-node | package.json | error |
-| @backstage/config | package.json | error |
-| better-sqlite3 | package.json | error |
-| node-gyp | package.json | error |
-| app | package.json | error |
-| pg | package.json | error |
diff --git a/workspaces/homepage/packages/backend/src/index.ts b/workspaces/homepage/packages/backend/src/index.ts
index 4e318c2d0..6ad47ba7c 100644
--- a/workspaces/homepage/packages/backend/src/index.ts
+++ b/workspaces/homepage/packages/backend/src/index.ts
@@ -27,6 +27,7 @@ backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
backend.add(import('@backstage/plugin-auth-backend'));
// See https://backstage.io/docs/backend-system/building-backends/migrating#the-auth-plugin
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
+backend.add(import('@backstage/plugin-auth-backend-module-github-provider'));
// See https://backstage.io/docs/auth/guest/provider
// catalog plugin
diff --git a/workspaces/homepage/plugins/dynamic-home-page/dev/index.tsx b/workspaces/homepage/plugins/dynamic-home-page/dev/index.tsx
index d6e27d8b0..2d34685e7 100644
--- a/workspaces/homepage/plugins/dynamic-home-page/dev/index.tsx
+++ b/workspaces/homepage/plugins/dynamic-home-page/dev/index.tsx
@@ -44,9 +44,10 @@ import { ScalprumContext, ScalprumState } from '@scalprum/react-core';
import { QuickAccessApi, quickAccessApiRef } from '../src/api';
import {
+ dynamicHomePagePlugin,
CatalogStarredEntitiesCard,
DynamicHomePage,
- dynamicHomePagePlugin,
+ DynamicHomePageProps,
FeaturedDocsCard,
Headline,
JokeCard,
@@ -57,6 +58,7 @@ import {
RecentlyVisitedCard,
SearchBar,
TopVisitedCard,
+ WorldClock,
} from '../src/plugin';
import { HomePageCardMountPoint, QuickAccessLink } from '../src/types';
import defaultQuickAccess from './quickaccess-default.json';
@@ -193,11 +195,13 @@ class MockVisitsApi implements VisitsApi {
const createPage = ({
navTitle,
pageTitle,
+ props,
pageWidth,
mountPoints,
}: {
navTitle: string;
pageTitle?: string;
+ props?: DynamicHomePageProps;
pageWidth?: number;
mountPoints?: HomePageCardMountPoint[];
}): DevAppPageOptions => {
@@ -228,7 +232,7 @@ const createPage = ({
-
+
@@ -244,24 +248,6 @@ const createPage = ({
createDevApp()
.registerPlugin(dynamicHomePagePlugin)
.addThemes(getAllThemes())
- .addPage({
- path: '/catalog',
- title: 'Catalog',
- element: ,
- })
- .addPage({
- path: '/catalog/:namespace/:kind/:name',
- element: ,
- children: (
-
-
-
- Overview
-
-
-
- ),
- })
.addPage(
createPage({
navTitle: 'Default',
@@ -289,6 +275,99 @@ createDevApp()
mountPoints: defaultMountPoints,
}),
)
+ .addPage(
+ createPage({
+ navTitle: 'With title',
+ props: {
+ title: 'Hello {{firstName}}',
+ },
+ mountPoints: defaultMountPoints,
+ }),
+ )
+ .addPage(
+ createPage({
+ navTitle: 'With subtitle',
+ props: {
+ subtitle: 'Hello {{displayName}}',
+ },
+ mountPoints: defaultMountPoints,
+ }),
+ )
+ .addPage(
+ createPage({
+ navTitle: 'With pageTitle',
+ props: {
+ pageTitle: 'Another page title',
+ },
+ mountPoints: defaultMountPoints,
+ }),
+ )
+ .addPage(
+ createPage({
+ navTitle: 'With local clock',
+ props: {
+ localClock: {
+ format: 'full',
+ },
+ },
+ mountPoints: defaultMountPoints,
+ }),
+ )
+ .addPage(
+ createPage({
+ navTitle: 'With world clocks',
+ props: {
+ worldClocks: [
+ {
+ label: 'Raleigh',
+ timeZone: 'EST',
+ },
+ {
+ label: 'London',
+ timeZone: 'GMT',
+ },
+ {
+ label: 'Brno',
+ timeZone: 'CET',
+ },
+ {
+ label: 'Bangalore',
+ timeZone: 'IST',
+ },
+ ],
+ },
+ mountPoints: defaultMountPoints,
+ }),
+ )
+ .addPage(
+ createPage({
+ navTitle: 'With both clocks',
+ props: {
+ localClock: {
+ format: 'time',
+ },
+ worldClocks: [
+ {
+ label: 'Raleigh',
+ timeZone: 'EST',
+ },
+ {
+ label: 'London',
+ timeZone: 'GMT',
+ },
+ {
+ label: 'Brno',
+ timeZone: 'CET',
+ },
+ {
+ label: 'Bangalore',
+ timeZone: 'IST',
+ },
+ ],
+ },
+ mountPoints: defaultMountPoints,
+ }),
+ )
.addPage(
createPage({
navTitle: 'No configuration',
@@ -566,6 +645,45 @@ createDevApp()
],
}),
)
+ .addPage(
+ createPage({
+ navTitle: 'WorldClock',
+ pageTitle: 'WorldClock',
+ mountPoints: [
+ {
+ Component: WorldClock as React.ComponentType,
+ config: {
+ props: {
+ worldClocks: [
+ {
+ label: 'Raleigh',
+ timeZone: 'EST',
+ },
+ {
+ label: 'London',
+ timeZone: 'GMT',
+ },
+ {
+ label: 'Brno',
+ timeZone: 'CET',
+ },
+ {
+ label: 'Bangalore',
+ timeZone: 'IST',
+ },
+ ],
+ timeFormat: {
+ day: '2-digit',
+ month: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ },
+ },
+ },
+ },
+ ],
+ }),
+ )
.addPage(
createPage({
navTitle: 'Layout test 1',
@@ -778,4 +896,22 @@ createDevApp()
],
}),
)
+ .addPage({
+ path: '/catalog',
+ title: 'Catalog',
+ element: ,
+ })
+ .addPage({
+ path: '/catalog/:namespace/:kind/:name',
+ element: ,
+ children: (
+
+
+
+ Overview
+
+
+
+ ),
+ })
.render();
diff --git a/workspaces/homepage/plugins/dynamic-home-page/knip-report.md b/workspaces/homepage/plugins/dynamic-home-page/knip-report.md
index 8a1d94ded..1a4f92a80 100644
--- a/workspaces/homepage/plugins/dynamic-home-page/knip-report.md
+++ b/workspaces/homepage/plugins/dynamic-home-page/knip-report.md
@@ -14,3 +14,4 @@
| @testing-library/user-event | package.json | error |
| @backstage/core-app-api | package.json | error |
| msw | package.json | error |
+
diff --git a/workspaces/homepage/plugins/dynamic-home-page/report.api.md b/workspaces/homepage/plugins/dynamic-home-page/report.api.md
index a3c6ac195..97da25507 100644
--- a/workspaces/homepage/plugins/dynamic-home-page/report.api.md
+++ b/workspaces/homepage/plugins/dynamic-home-page/report.api.md
@@ -5,6 +5,7 @@
```ts
import { BackstagePlugin } from '@backstage/core-plugin-api';
+import { ClockConfig } from '@backstage/plugin-home';
import { FeaturedDocsCardProps } from '@backstage/plugin-home';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
@@ -22,10 +23,20 @@ export const dynamicHomePagePlugin: BackstagePlugin< {
root: RouteRef;
}, {}, {}>;
-// @public (undocumented)
+// @public
export interface DynamicHomePageProps {
+ // (undocumented)
+ localClock?: LocalClockProps;
+ // (undocumented)
+ pageTitle?: string;
+ // (undocumented)
+ personalizedTitle?: string;
+ // (undocumented)
+ subtitle?: string;
// (undocumented)
title?: string;
+ // (undocumented)
+ worldClocks?: ClockConfig[];
}
// @public (undocumented)
@@ -47,6 +58,16 @@ export const JokeCard: React_2.ComponentType<{
defaultCategory?: 'any' | 'programming';
}>;
+// @public (undocumented)
+export interface LocalClockProps {
+ // (undocumented)
+ format?: 'none' | 'full' | 'date' | 'datewithweekday' | 'time' | 'timewithseconds' | 'both';
+ // (undocumented)
+ label?: string;
+ // (undocumented)
+ lang?: string;
+}
+
// @public (undocumented)
export const Markdown: React_2.ComponentType;
@@ -111,4 +132,17 @@ export const TopVisitedCard: React_2.ComponentType;
// @public (undocumented)
export const VisitListener: () => React_2.JSX.Element | null;
+// @public (undocumented)
+export const WorldClock: ({ worldClocks, timeFormat, justifyContent, }: WorldClockProps) => React_2.JSX.Element;
+
+// @public (undocumented)
+export interface WorldClockProps {
+ // (undocumented)
+ justifyContent?: 'space-between' | 'space-around';
+ // (undocumented)
+ timeFormat?: Intl.DateTimeFormatOptions;
+ // (undocumented)
+ worldClocks: ClockConfig[];
+}
+
```
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/components/DynamicHomePage.tsx b/workspaces/homepage/plugins/dynamic-home-page/src/components/DynamicHomePage.tsx
index 4e204db3e..92f9d41e0 100644
--- a/workspaces/homepage/plugins/dynamic-home-page/src/components/DynamicHomePage.tsx
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/components/DynamicHomePage.tsx
@@ -14,57 +14,33 @@
* limitations under the License.
*/
-import React, { useMemo } from 'react';
+import React from 'react';
-import { Content, EmptyState, Header, Page } from '@backstage/core-components';
+import type { ClockConfig } from '@backstage/plugin-home';
-import { useHomePageMountPoints } from '../hooks/useHomePageMountPoints';
-import { ReadOnlyGrid } from './ReadOnlyGrid';
+import { useDynamicHomePageCards } from '../hooks/useDynamicHomePageCards';
+import { HomePage } from './HomePage';
+import type { LocalClockProps } from './LocalClock';
/**
+ * This type is similar to Omit<HomePageProps, 'cards'>.
+ * We redefine it here to avoid the need to export HomePageProps to the API export!
* @public
*/
export interface DynamicHomePageProps {
title?: string;
+ personalizedTitle?: string;
+ pageTitle?: string;
+ subtitle?: string;
+ localClock?: LocalClockProps;
+ worldClocks?: ClockConfig[];
}
/**
* @public
*/
export const DynamicHomePage = (props: DynamicHomePageProps) => {
- const allHomePageMountPoints = useHomePageMountPoints();
+ const cards = useDynamicHomePageCards();
- const filteredAndSortedHomePageCards = useMemo(() => {
- if (!allHomePageMountPoints) {
- return [];
- }
-
- const filteredAndSorted = allHomePageMountPoints.filter(
- card =>
- card.enabled !== false &&
- (!card.config?.priority || card.config.priority >= 0),
- );
-
- filteredAndSorted.sort(
- (a, b) => (b.config?.priority ?? 0) - (a.config?.priority ?? 0),
- );
-
- return filteredAndSorted;
- }, [allHomePageMountPoints]);
-
- return (
-
-
-
- {filteredAndSortedHomePageCards.length === 0 ? (
-
- ) : (
-
- )}
-
-
- );
+ return ;
};
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/components/Headline.test.tsx b/workspaces/homepage/plugins/dynamic-home-page/src/components/Headline.test.tsx
new file mode 100644
index 000000000..b8cdc8bd2
--- /dev/null
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/components/Headline.test.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { Headline } from './Headline';
+
+describe('Headline', () => {
+ it('renders successfully', async () => {
+ render();
+
+ expect(screen.getByText('This is a headline')).toBeInTheDocument();
+ });
+});
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/components/HomePage.tsx b/workspaces/homepage/plugins/dynamic-home-page/src/components/HomePage.tsx
new file mode 100644
index 000000000..a73c029c6
--- /dev/null
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/components/HomePage.tsx
@@ -0,0 +1,140 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useMemo } from 'react';
+
+import { identityApiRef, useApi } from '@backstage/core-plugin-api';
+import { Content, EmptyState, Header, Page } from '@backstage/core-components';
+import { ClockConfig, HeaderWorldClock } from '@backstage/plugin-home';
+
+import useAsync from 'react-use/esm/useAsync';
+
+import { HomePageCardMountPoint } from '../types';
+import { ReadOnlyGrid } from './ReadOnlyGrid';
+import { LocalClock, LocalClockProps } from './LocalClock';
+
+export interface HomePageProps {
+ title?: string;
+ personalizedTitle?: string;
+ pageTitle?: string;
+ subtitle?: string;
+ localClock?: LocalClockProps;
+ worldClocks?: ClockConfig[];
+ cards?: HomePageCardMountPoint[];
+}
+
+// I kept this because I hope that we will add this soon or at least in Dynamic Home Page plugin 1.2 ~ RHDH 1.6.
+// const getTimeBasedTitle = (): string => {
+// const currentHour = new Date(Date.now()).getHours();
+// if (currentHour < 12) {
+// return 'Good morning {{firstName}}';
+// } else if (currentHour < 17) {
+// return 'Good afternoon {{firstName}}';
+// }
+// return 'Good evening {{firstName}}';
+// };
+
+const getPersonalizedTitle = (
+ title: string,
+ displayName: string | undefined,
+) => {
+ const firstName = displayName?.split(' ')[0];
+ const replacedTitle = title
+ .replace('{{firstName}}', firstName ?? '')
+ .replace('{{displayName}}', displayName ?? '');
+ return replacedTitle;
+};
+
+export const HomePage = (props: HomePageProps) => {
+ const identityApi = useApi(identityApiRef);
+ const { value: profile } = useAsync(() => identityApi.getProfileInfo());
+
+ const title = React.useMemo(() => {
+ if (profile?.displayName && props.personalizedTitle) {
+ return getPersonalizedTitle(props.personalizedTitle, profile.displayName);
+ } else if (props.title) {
+ return getPersonalizedTitle(props.title, profile?.displayName);
+ }
+ // return getPersonalizedTitle(getTimeBasedTitle(), profile?.displayName);
+ return getPersonalizedTitle('Welcome back!', profile?.displayName);
+ }, [profile?.displayName, props.personalizedTitle, props.title]);
+
+ const subtitle = React.useMemo(() => {
+ return props.subtitle
+ ? getPersonalizedTitle(props.subtitle, profile?.displayName)
+ : undefined;
+ }, [props.subtitle, profile?.displayName]);
+
+ const filteredAndSortedHomePageCards = useMemo(() => {
+ if (!props.cards) {
+ return [];
+ }
+
+ const filteredAndSorted = props.cards.filter(
+ card =>
+ card.enabled !== false &&
+ (!card.config?.priority || card.config.priority >= 0),
+ );
+
+ filteredAndSorted.sort(
+ (a, b) => (b.config?.priority ?? 0) - (a.config?.priority ?? 0),
+ );
+
+ return filteredAndSorted;
+ }, [props.cards]);
+
+ return (
+
+
+ {props.localClock?.format && props.localClock?.format !== 'none' ? (
+ 0
+ ? 'Local'
+ : undefined)
+ }
+ format={
+ props.localClock?.format ??
+ (props.worldClocks && props.worldClocks.length > 0
+ ? 'time'
+ : undefined)
+ }
+ lang={props.localClock?.lang}
+ />
+ ) : null}
+
+ {props.worldClocks && props.worldClocks.length > 0 ? (
+
+ ) : null}
+
+
+ {filteredAndSortedHomePageCards.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/components/LocalClock.test.tsx b/workspaces/homepage/plugins/dynamic-home-page/src/components/LocalClock.test.tsx
new file mode 100644
index 000000000..c208b5a78
--- /dev/null
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/components/LocalClock.test.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { LocalClock } from './LocalClock';
+
+jest.mock('@backstage/core-components', () => ({
+ ...jest.requireActual('@backstage/core-components'),
+ HeaderLabel: ({ label, value }: { label: string; value?: string }) => (
+
+ ),
+}));
+
+describe('LocalClock', () => {
+ beforeAll(() => {
+ jest.useFakeTimers().setSystemTime(new Date('2025-01-01 13:14:15'));
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('renders successfully', () => {
+ render();
+ expect(screen.getByText('01:14 PM')).toBeInTheDocument();
+ });
+
+ it.skip('renders nothing if format is none', () => {
+ render();
+ expect(screen.getByTestId).toBeInTheDocument();
+ });
+
+ const formats = {
+ full: 'Wednesday, 01/01/2025, 01:14 PM',
+ date: '01/01/2025',
+ datewithweekday: 'Wednesday, 01/01/2025',
+ time: '01:14 PM',
+ timewithseconds: '01:14:15 PM',
+ both: '01/01/2025, 01:14 PM',
+ };
+
+ for (const [format, expectedDateTime] of Object.entries(formats)) {
+ it(`renders format ${format} correctly`, () => {
+ render();
+ expect(screen.getByText(expectedDateTime)).toBeInTheDocument();
+ });
+ }
+});
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/components/LocalClock.tsx b/workspaces/homepage/plugins/dynamic-home-page/src/components/LocalClock.tsx
new file mode 100644
index 000000000..4e655570f
--- /dev/null
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/components/LocalClock.tsx
@@ -0,0 +1,116 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+
+import { HeaderLabel } from '@backstage/core-components';
+
+/**
+ * @public
+ */
+export interface LocalClockProps {
+ label?: string;
+ format?:
+ | 'none'
+ | 'full'
+ | 'date'
+ | 'datewithweekday'
+ | 'time'
+ | 'timewithseconds'
+ | 'both';
+ lang?: string;
+}
+
+/**
+ * @public
+ */
+export const LocalClock = (props: LocalClockProps) => {
+ const format = props.format ?? 'time';
+ const lang = props.lang ?? window.navigator.language;
+
+ const [time, setTime] = React.useState(() => new Date());
+
+ // Could be optimized to only update the time when needed, but it's aligned with
+ // https://github.com/backstage/backstage/blob/master/plugins/home/src/homePageComponents/HeaderWorldClock/HeaderWorldClock.tsx for now
+ React.useEffect(() => {
+ if (format === 'none') {
+ return () => null;
+ }
+ const intervalId = setInterval(() => setTime(new Date()), 1000);
+ return () => clearInterval(intervalId);
+ }, [format]);
+
+ if (format === 'none') {
+ return null;
+ }
+
+ try {
+ const includeDate =
+ format === 'full' ||
+ format === 'date' ||
+ format === 'datewithweekday' ||
+ format === 'both';
+ const includeTime =
+ format === 'full' ||
+ format === 'time' ||
+ format === 'timewithseconds' ||
+ format === 'both';
+
+ const value =
+ format === 'date' || format === 'datewithweekday'
+ ? time.toLocaleDateString(lang, {
+ weekday: format === 'datewithweekday' ? 'long' : undefined,
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ })
+ : time.toLocaleTimeString(lang, {
+ weekday: format === 'full' ? 'long' : undefined,
+ day: includeDate ? '2-digit' : undefined,
+ month: includeDate ? '2-digit' : undefined,
+ year: includeDate ? 'numeric' : undefined,
+ hour: includeTime ? '2-digit' : undefined,
+ minute: includeTime ? '2-digit' : undefined,
+ second: format === 'timewithseconds' ? '2-digit' : undefined,
+ });
+ const dateTime = time.toLocaleTimeString(lang, {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ });
+
+ return (
+
+ {value}
+
+ }
+ />
+ );
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.warn('Failed to render clock', e);
+ return null;
+ }
+};
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/components/Markdown.test.tsx b/workspaces/homepage/plugins/dynamic-home-page/src/components/Markdown.test.tsx
new file mode 100644
index 000000000..0bd5b5521
--- /dev/null
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/components/Markdown.test.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { Markdown } from './Markdown';
+
+describe('Markdown', () => {
+ it('renders successfully', async () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText('This is a headline')).toBeInTheDocument();
+ expect(screen.getByText('This is some markdown')).toBeInTheDocument();
+ expect(screen.getByText('Some content')).toBeInTheDocument();
+ });
+});
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/components/MarkdownCard.test.tsx b/workspaces/homepage/plugins/dynamic-home-page/src/components/MarkdownCard.test.tsx
new file mode 100644
index 000000000..562b9b06e
--- /dev/null
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/components/MarkdownCard.test.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { MarkdownCard } from './MarkdownCard';
+
+describe('MarkdownCard', () => {
+ it('renders successfully', async () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText('This is a headline')).toBeInTheDocument();
+ expect(screen.getByText('This is some markdown')).toBeInTheDocument();
+ expect(screen.getByText('Some content')).toBeInTheDocument();
+ });
+});
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/components/Placeholder.test.tsx b/workspaces/homepage/plugins/dynamic-home-page/src/components/Placeholder.test.tsx
new file mode 100644
index 000000000..585c7dad8
--- /dev/null
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/components/Placeholder.test.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { Placeholder } from './Placeholder';
+
+describe('Placeholder', () => {
+ it('renders successfully', async () => {
+ render();
+
+ expect(screen.getByText('Debug content')).toBeInTheDocument();
+ });
+});
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/components/VisitListener.tsx b/workspaces/homepage/plugins/dynamic-home-page/src/components/VisitListener.tsx
index ecc72425f..9ffc1fb12 100644
--- a/workspaces/homepage/plugins/dynamic-home-page/src/components/VisitListener.tsx
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/components/VisitListener.tsx
@@ -17,13 +17,13 @@
import React from 'react';
import { VisitListener as VisitListenerComponent } from '@backstage/plugin-home';
-import { useHomePageMountPoints } from '../hooks/useHomePageMountPoints';
+import { useDynamicHomePageCards } from '../hooks/useDynamicHomePageCards';
export const VisitListener = () => {
- const allHomePageMountPoints = useHomePageMountPoints();
+ const cards = useDynamicHomePageCards();
const shouldLoadVisitListener = React.useMemo(() => {
- if (!allHomePageMountPoints) {
+ if (!cards) {
return false;
}
@@ -32,10 +32,10 @@ export const VisitListener = () => {
'Extension(TopVisitedCard)',
];
- return allHomePageMountPoints.some(card =>
+ return cards.some(card =>
requiresVisitListener.includes(card.Component.displayName!),
);
- }, [allHomePageMountPoints]);
+ }, [cards]);
return shouldLoadVisitListener ? : null;
};
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/components/WorldClock.tsx b/workspaces/homepage/plugins/dynamic-home-page/src/components/WorldClock.tsx
new file mode 100644
index 000000000..803776aa2
--- /dev/null
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/components/WorldClock.tsx
@@ -0,0 +1,46 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+
+import { ClockConfig, HeaderWorldClock } from '@backstage/plugin-home';
+
+/**
+ * @public
+ */
+export interface WorldClockProps {
+ worldClocks: ClockConfig[];
+ timeFormat?: Intl.DateTimeFormatOptions;
+ justifyContent?: 'space-between' | 'space-around';
+}
+
+/**
+ * @public
+ */
+export const WorldClock = ({
+ worldClocks,
+ timeFormat,
+ justifyContent = 'space-between',
+}: WorldClockProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/hooks/useHomePageMountPoints.ts b/workspaces/homepage/plugins/dynamic-home-page/src/hooks/useDynamicHomePageCards.ts
similarity index 91%
rename from workspaces/homepage/plugins/dynamic-home-page/src/hooks/useHomePageMountPoints.ts
rename to workspaces/homepage/plugins/dynamic-home-page/src/hooks/useDynamicHomePageCards.ts
index 439b7e411..17ed807c5 100644
--- a/workspaces/homepage/plugins/dynamic-home-page/src/hooks/useHomePageMountPoints.ts
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/hooks/useDynamicHomePageCards.ts
@@ -28,13 +28,13 @@ interface ScalprumState {
};
}
-export const useHomePageMountPoints = ():
+export const useDynamicHomePageCards = ():
| HomePageCardMountPoint[]
| undefined => {
const scalprum = useScalprum();
- const homePageMountPoints =
+ const cards =
scalprum?.api?.dynamicRootConfig?.mountPoints?.['home.page/cards'];
- return homePageMountPoints;
+ return cards;
};
diff --git a/workspaces/homepage/plugins/dynamic-home-page/src/plugin.ts b/workspaces/homepage/plugins/dynamic-home-page/src/plugin.ts
index e4b74cf55..07930f11a 100644
--- a/workspaces/homepage/plugins/dynamic-home-page/src/plugin.ts
+++ b/workspaces/homepage/plugins/dynamic-home-page/src/plugin.ts
@@ -53,6 +53,8 @@ export type { HeadlineProps } from './components/Headline';
export type { MarkdownProps } from './components/Markdown';
export type { MarkdownCardProps } from './components/MarkdownCard';
export type { PlaceholderProps } from './components/Placeholder';
+export type { LocalClockProps } from './components/LocalClock';
+export type { WorldClockProps } from './components/WorldClock';
/**
* Dynamic Home Page Plugin
@@ -263,3 +265,15 @@ export const VisitListener = dynamicHomePagePlugin.provide(
},
}),
);
+
+/**
+ * @public
+ */
+export const WorldClock = dynamicHomePagePlugin.provide(
+ createComponentExtension({
+ name: 'WorldClock',
+ component: {
+ lazy: () => import('./components/WorldClock').then(m => m.WorldClock),
+ },
+ }),
+);