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 }) => ( +
+
{label}
+
{value}
+
+ ), +})); + +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), + }, + }), +);