Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

🌱 Setup branding to be configurable during the build (#1664, #1742) #1738

Merged
merged 3 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions BRANDING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Branding

The UI supports static branding at build time. Dynamically switching brands is not
possible with the current implementation.

## Summary

Each of the project modules need to do some branding enablement.

- `@konveyor-ui/common` pulls in the branding assets and packages the configuration,
strings and assets within the common package. The other modules pull branding
from the common module.

- `@konveyor-ui/client` uses branding from the common package:

- The location of `favicon.ico`, `manifest.json` and any other branding
assets that may be referenced in the `brandingStrings` are sourced from the
common package.

- The `brandingStrings` are used by the dev-server runtime, to fill out the
`index.html` template.

- The about modal and application masthead components use the branding strings
provided by the common module to display brand appropriate logos, titles and
about information. Since the common module provides all the information, it
is packaged directly into the app at build time.

- `@konveyor-ui/server` uses the `brandingStrings` from the common package to fill
out the `index.html` template.

## Providing alternate branding

To provide an alternate branding to the build, specify the path to the branding assets
with the `BRANDING` environment variable. Relative paths in `BRANDING` are computed
from the project source root.

Each brand requires the presence of at least the following files:

- `strings.json`
- `favicon.ico`
- `manifest.json`

With a file path of `/alt/custom-branding`, a build that uses an alternate branding
is run as:

```sh
> BRANDING=/alt/custom-branding npm run build
```

The dev server can also be run this way. Since file watching of the branding assets
is not implemented in the common module's build watch mode, it may be necessary to
manually build the common module before running the dev server. When working on a
brand, it is useful to run the dev server like this:

```sh
> export BRANDING=/alt/custom-branding
> npm run build -w common
> npm run start:dev
> unset BRANDING # when you don't want to use the custom branding path anymore
```

### File details

#### strings.json

The expected shape of `strings.json` is defined in [branding.ts](./common/src/branding.ts).

The default version of the file is [branding/strings.json](./branding/strings.json).

A minimal viable example of the file is:

```json
{
"application": {
"title": "Konveyor"
},
"about": {
"displayName": "Konveyor"
},
"masthead": {}
}
```

At build time, the json file is processed as an [ejs](https://ejs.co/) template. The
variable `brandingRoot` is provided as the relative root of the branding
assets within the build of the common module. Consider the location of `strings.json`
in your branding directory as the base `brandingRoot` when creating a new brand.

For example, to properly reference a logo within this branding structure:

```
special-brand/
images/
masthead-logo.svg
about-logo.svg
strings.json
```

Use a url string like this:

```json
{
"about": {
"imageSrc": "<%= brandingRoot %>/images/about-logo.svg"
}
}
```

and in the output of `BRANDING=special-brand npm run build -w common`, the `imageSrc`
will be `branding/images/about-logo.svg` with all of the files in `special-branding/*`
copied to and available to the client and server modules from
`@konveyor-ui/common/branding/*`.

#### favicon.ico

A standard favorite icon file `favicon.ico` is required to be in the same directory
as `strings.json`

#### manifest.json

A standard [web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest)
file `manifest.json` is required to be in the same directory as `strings.json`.

## Technical details

All branding strings and assets are pulled in to the common module. The client and
server modules access the branding from the common module build.

The `common` module relies on rollup packaging to embed all of the brand for easy
use. The use of branding strings in `client` and `server` modules is straight forward.
Pulling in `strings.json` and providing the base path to the brand assets is a
more complicated.

The `common` module provides the `brandingAssetPath()` function to let the build time
code find the root path to all brand assets. Webpack configuration files use this
function to source the favicon.ico, manifest.json and other brand assets to be copied
to the application bundle.

The `brandingStrings` is typed and sourced from a json file. To pass typescript builds,
a stub json file needs to be available at transpile time. By using a typescript paths
of `@branding/strings.json`, the stub json is found at transpile time. The generated
javascript will still import the path alias. The
[virtual rollup plugin](https://github.com/rollup/plugins/tree/master/packages/virtual)
further transform the javascript output by replacing the `@branding/strings.json` import
with a dynamically built module containing the contents of the brand's `strings.json`.
The brand json becomes a virtual module embedded in the common module.

A build for a custom brand will fail (1) if the expected files cannot be read, or (2)
if `strings.json` is not a valid JSON file. **Note:** The context of `stings.json` is
not currently validated. If something is missing or a url is malformed, it will only
be visible as a runtime error.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
4 changes: 2 additions & 2 deletions client/public/manifest.json → branding/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"short_name": "tackle-ui",
"name": "Tackle UI",
"short_name": "konveyor-ui",
"name": "Konveyor UI",
"icons": [
{
"src": "favicon.ico",
Expand Down
21 changes: 21 additions & 0 deletions branding/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"application": {
"title": "Konveyor",
"name": "Konveyor Tackle UI",
"description": "Konveyor/Tackle UI"
},
"about": {
"displayName": "Konveyor",
"imageSrc": "<%= brandingRoot %>/images/masthead-logo.svg",
"documentationUrl": "https://konveyor.github.io/konveyor/"
},
"masthead": {
"leftBrand": {
"src": "<%= brandingRoot %>/images/masthead-logo.svg",
"alt": "brand",
"height": "60px"
},
"leftTitle": null,
"rightBrand": null
}
}
68 changes: 68 additions & 0 deletions client/__mocks__/react-i18next.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-env node */

// Adapted from https://github.com/i18next/react-i18next/blob/master/example/test-jest/src/__mocks__/react-i18next.js
import React from "react";
import * as reactI18next from "react-i18next";

const hasChildren = (node) =>
node && (node.children || (node.props && node.props.children));

const getChildren = (node) =>
node && node.children ? node.children : node.props && node.props.children;

const renderNodes = (reactNodes) => {
if (typeof reactNodes === "string") {
return reactNodes;
}

return Object.keys(reactNodes).map((key, i) => {
const child = reactNodes[key];
const isElement = React.isValidElement(child);

if (typeof child === "string") {
return child;
}
if (hasChildren(child)) {
const inner = renderNodes(getChildren(child));
return React.cloneElement(child, { ...child.props, key: i }, inner);
}
if (typeof child === "object" && !isElement) {
return Object.keys(child).reduce(
(str, childKey) => `${str}${child[childKey]}`,
""
);
}

return child;
});
};

const useMock = [(k) => k, { changeLanguage: () => new Promise(() => {}) }];
useMock.t = (k) => k;
useMock.i18n = { changeLanguage: () => new Promise(() => {}) };

module.exports = {
Trans: ({ children, i18nKey }) =>
!children
? i18nKey
: Array.isArray(children)
? renderNodes(children)
: renderNodes([children]),

Translation: ({ children }) => children((k) => k, { i18n: {} }),

useTranslation: () => useMock,

initReactI18next: {
type: "3rdParty",
init: () => {},
},

// mock if needed
withTranslation: reactI18next.withTranslation,
I18nextProvider: reactI18next.I18nextProvider,
setDefaults: reactI18next.setDefaults,
getDefaults: reactI18next.getDefaults,
setI18n: reactI18next.setI18n,
getI18n: reactI18next.getI18n,
};
5 changes: 4 additions & 1 deletion client/config/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const config: JestConfigWithTsJest = {
"@patternfly/react-icons/dist/esm/icons/":
"<rootDir>/__mocks__/fileMock.js",

// other mocks
"react-i18next": "<rootDir>/__mocks__/react-i18next.js",

// match the paths in tsconfig.json
"@app/(.*)": "<rootDir>/src/app/$1",
"@assets/(.*)":
Expand All @@ -44,7 +47,7 @@ const config: JestConfigWithTsJest = {
},

// Code to set up the testing framework before each test file in the suite is executed
setupFilesAfterEnv: ["<rootDir>/src/app/setupTests.ts"],
setupFilesAfterEnv: ["<rootDir>/src/app/test-config/setupTests.ts"],
};

export default config;
31 changes: 21 additions & 10 deletions client/config/webpack.common.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import path from "path";
import { Configuration } from "webpack";
// import CaseSensitivePathsWebpackPlugin from "case-sensitive-paths-webpack-plugin";
import CopyPlugin from "copy-webpack-plugin";
import Dotenv from "dotenv-webpack";
import { TsconfigPathsPlugin } from "tsconfig-paths-webpack-plugin";
import MonacoWebpackPlugin from "monaco-editor-webpack-plugin";

import { brandingAssetPath } from "@konveyor-ui/common";
import { LANGUAGES_BY_FILE_EXTENSION } from "./monacoConstants";

const BG_IMAGES_DIRNAME = "images";
const pathTo = (relativePath: string) => path.resolve(__dirname, relativePath);
const brandingPath = brandingAssetPath();
const manifestPath = path.resolve(brandingPath, "manifest.json");

const BG_IMAGES_DIRNAME = "images";

const config: Configuration = {
entry: {
Expand Down Expand Up @@ -150,22 +153,26 @@ const config: Configuration = {
exports: "xmllint",
},
},
// For monaco-editor-webpack-plugin
{
test: /\.yaml$/,
use: "raw-loader",
},

// For monaco-editor-webpack-plugin --->
{
test: /\.css$/,
include: [pathTo("../../node_modules/monaco-editor")],
use: ["style-loader", "css-loader"],
},
// For monaco-editor-webpack-plugin
{
test: /\.ttf$/,
type: "asset/resource",
},
// <--- For monaco-editor-webpack-plugin
],
},

plugins: [
// new CaseSensitivePathsWebpackPlugin(),
new Dotenv({
systemvars: true,
silent: true,
Expand All @@ -174,15 +181,19 @@ const config: Configuration = {
patterns: [
{
from: pathTo("../public/locales"),
to: pathTo("../dist/locales"),
to: "./locales/",
},
{
from: pathTo("../public/manifest.json"),
to: pathTo("../dist/manifest.json"),
from: pathTo("../public/templates"),
to: "./templates/",
},
{
from: pathTo("../public/templates"),
to: pathTo("../dist/templates"),
from: manifestPath,
to: ".",
},
{
from: brandingPath,
to: "./branding/",
},
],
}),
Expand Down
Loading
Loading