From 2ea058482a6d61f13b51160a57687f8ad3c83d9d Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Thu, 14 Nov 2024 00:53:34 +0200 Subject: [PATCH 1/3] feat(e2e): implemented test to check download users link (#1827) Signed-off-by: Oleksandr Andriienko --- .../playwright/e2e/plugins/rbac/rbac.spec.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/e2e-tests/playwright/e2e/plugins/rbac/rbac.spec.ts b/e2e-tests/playwright/e2e/plugins/rbac/rbac.spec.ts index e257979af..de82e2dd9 100644 --- a/e2e-tests/playwright/e2e/plugins/rbac/rbac.spec.ts +++ b/e2e-tests/playwright/e2e/plugins/rbac/rbac.spec.ts @@ -9,6 +9,7 @@ import { import { Roles } from "../../../support/pages/rbac"; import { Common, setupBrowser } from "../../../utils/Common"; import { UIhelper } from "../../../utils/UIhelper"; +import fs from "fs/promises"; test.describe .serial("Test RBAC plugin: load permission policies and conditions from files", () => { @@ -162,6 +163,42 @@ test.describe.serial("Test RBAC plugin as an admin user", () => { await uiHelper.verifyCellsInTable(allCellsIdentifier); }); + test("Should download the user list", async () => { + await page.locator('a:has-text("Download User List")').click(); + const fileContent = await downloadAndReadFile(page); + const lines = fileContent.trim().split("\n"); + + const header = "userEntityRef,displayName,email,lastAuthTime"; + if (lines[0] !== header) { + throw new Error("Header does not match"); + } + + // Check that each subsequent line starts with "user:default" + const allUsersValid = lines + .slice(1) + .every((line) => line.startsWith("user:default")); + if (!allUsersValid) { + throw new Error("Not all users info are valid"); + } + }); + + async function downloadAndReadFile(page: Page): Promise { + const [download] = await Promise.all([ + page.waitForEvent("download"), + page.locator('a:has-text("Download User List")').click(), + ]); + + const filePath = await download.path(); + + if (filePath) { + const fileContent = await fs.readFile(filePath, "utf-8"); + return fileContent; + } else { + console.error("Download failed or path is not available"); + return undefined; + } + } + test("View details of a role", async () => { await uiHelper.clickLink("role:default/rbac_admin"); From ecd0fb9293dd70393d866003c28082acb0e5f528 Mon Sep 17 00:00:00 2001 From: Armel Soro Date: Wed, 13 Nov 2024 23:53:41 +0100 Subject: [PATCH 2/3] fix(corporate-proxy): use the built-in `EnvHttpProxyAgent` from `undici` to fix the proxy issues reported (#1903) * Use the built-in `EnvHttpProxyAgent` from `undici` Undici has added native support for proxy handling, so it is no longer necessary for us to have our own custom proxy handling for 'fetch' calls. This was introduced in Undici v6.14.0 and enhanced in the next versions and should fix the issues faced with the handling of the 'NO_PROXY' env var by 'fetch'. [1][2] Note that 'global-agent' should still continue to handle proxy settings for 'node-fetch'. [1] https://github.com/nodejs/TSC/blob/main/meetings/2024-09-04.md?plain=1#L88-L115 [2] https://github.com/nodejs/undici/releases/tag/v6.14.0 * Add some docs and reference links to understand how the settings are handled * Set 'localhost,127.0.0.1' as default value for `NO_PROXY` For the backend to function correctly, requests to `localhost` need to bypass any proxy settings. Note that defining this env var will take effect only if the `HTTP(S)_PROXY` env vars are set. Co-authored-by: Marcel Hild --------- Co-authored-by: Marcel Hild --- .rhdh/docker/Dockerfile | 7 +- docker/Dockerfile | 7 +- docs/corporate-proxy.md | 26 ++++- docs/proxy.md | 120 ++++++++++++++++++++++-- packages/backend/src/corporate-proxy.ts | 77 +-------------- 5 files changed, 152 insertions(+), 85 deletions(-) diff --git a/.rhdh/docker/Dockerfile b/.rhdh/docker/Dockerfile index 60784659c..6ea60823b 100644 --- a/.rhdh/docker/Dockerfile +++ b/.rhdh/docker/Dockerfile @@ -364,13 +364,18 @@ ENV NPM_CONFIG_ignore-scripts='true' ENV SEGMENT_WRITE_KEY=gGVM6sYRK0D0ndVX22BOtS7NRcxPej8t ENV SEGMENT_TEST_MODE=false -# RHIDP-2217: corporate proxy support (configured using 'global-agent') +# RHIDP-2217: corporate proxy support (configured using 'global-agent' for 'node-fetch' calls and 'undici' for 'fetch' calls) # This is to avoid having to define several environment variables for the same purpose, # i.e, GLOBAL_AGENT_HTTP(S)_PROXY (for 'global-agent') and the conventional HTTP(S)_PROXY (honored by other libraries like Axios). # By setting GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE to an empty value, # 'global-agent' will use the same HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables. ENV GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE='' +# RHDHBUGS-106,RHIDP-4646: requests to the loopback interface should bypass the corporate proxy if set. +# Note that NO_PROXY will take effect only if the 'HTTP(S)_PROXY' environment variables are set. +# Users can still override this when running the image. +ENV NO_PROXY='localhost,127.0.0.1' + # The `--no-node-snapshot` node option enables the usage of the backstage scaffolder on nodejs 20 # https://github.com/backstage/backstage/issues/20661 diff --git a/docker/Dockerfile b/docker/Dockerfile index f825a5d3a..6517d4b4a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -281,13 +281,18 @@ ENV NPM_CONFIG_ignore-scripts='true' ENV SEGMENT_WRITE_KEY=gGVM6sYRK0D0ndVX22BOtS7NRcxPej8t ENV SEGMENT_TEST_MODE=false -# RHIDP-2217: corporate proxy support (configured using 'global-agent') +# RHIDP-2217: corporate proxy support (configured using 'global-agent' for 'node-fetch' calls and 'undici' for 'fetch' calls) # This is to avoid having to define several environment variables for the same purpose, # i.e, GLOBAL_AGENT_HTTP(S)_PROXY (for 'global-agent') and the conventional HTTP(S)_PROXY (honored by other libraries like Axios). # By setting GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE to an empty value, # 'global-agent' will use the same HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables. ENV GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE='' +# RHDHBUGS-106,RHIDP-4646: requests to the loopback interface should bypass the corporate proxy if set. +# Note that NO_PROXY will take effect only if the 'HTTP(S)_PROXY' environment variables are set. +# Users can still override this when running the image. +ENV NO_PROXY='localhost,127.0.0.1' + # The `--no-node-snapshot` node option enables the usage of the backstage scaffolder on nodejs 20 # https://github.com/backstage/backstage/issues/20661 diff --git a/docs/corporate-proxy.md b/docs/corporate-proxy.md index 0623ad0d6..89f41a988 100644 --- a/docs/corporate-proxy.md +++ b/docs/corporate-proxy.md @@ -5,7 +5,25 @@ Out of the box, the Showcase application can be run behind a corporate proxy, by - `HTTP_PROXY`: Proxy to use for HTTP requests. - `HTTPS_PROXY`: Proxy to use for HTTPS requests. -Additionally, you can set the `NO_PROXY` environment variable to exclude certain domains from proxying. The value is a comma-separated list of hostnames that do not require a proxy to get reached, even if one is specified. +Additionally, you can set the `NO_PROXY` environment variable to exclude certain domains from proxying. The value is a comma or space-separated list of hostnames that do not require a proxy to get reached, even if one is specified. + +## Understanding the `NO_PROXY` exclusion rules + +`NO_PROXY` is a comma or space-separated list of hostnames or IP addresses, optionally with port numbers. If the input URL matches any of the entries listed in `NO_PROXY`, then that URL will be fetched by a direct request (i.e., bypassing the proxy settings). + +Note that the default value for `NO_PROXY` in the container image is `localhost,127.0.0.1`. If you want to override it, please make sure to also include at least `localhost` or `localhost:7007` in the list; otherwise, the Backend might not work correctly. + +Matching follows the rules below: + +- `NO_PROXY=*` will bypass the proxy for all requests. +- Space and commas may be used to separate the entries in the `NO_PROXY` list. For example, `NO_PROXY=localhost,example.com`, `NO_PROXY="localhost example.com"`, or `NO_PROXY="localhost, example.com"` would have the same effect. +- If `NO_PROXY` does not contain any entries, then all requests will be sent through the proxy if the `HTTP(S)_PROXY` settings are configured. Otherwise, requests will be fetched directly. +- No DNS lookup is performed to decide if a request should bypass the proxy or not. For example, if DNS is known to resolve `example.com` to `1.2.3.4`, setting `NO_PROXY=1.2.3.4` will not have any effect on requests sent to `example.com`. Only requests explicitly sent to the IP address `1.2.3.4` will bypass the proxy. +- If a port is added after the host name or IP Address, then the input request must match both the host/IP and port in order to bypass the proxy. For example, `NO_PROXY=example.com:1234` would exclude requests to `http(s)://example.com:1234` (so calling them directly), but not requests to other ports like `http(s)://example.com` (which will be sent through the proxy). +- If no port is specified after the host name or IP address, all requests to that host/IP address will bypass the proxy regardless of the port. For example, `NO_PROXY=localhost` would exclude all requests sent to `localhost` (so calling them directly), like `http(s)://localhost:7077` and `http(s)://localhost:8888`. +- IP Address blocks in CIDR notation will not work. So setting `NO_PROXY=10.11.0.0/16` will not have any effect, even if a request is explicitly sent to an IP address in that block. +- Only IPv4 addresses are supported. IPv6 addresses like `::1` will not work. +- Generally, the proxy is only bypassed if the host name is an exact match for an entry in the `NO_PROXY` list. The only exceptions are entries that start with a dot (`.`) or with a wildcard (`*`). In such a case, the proxy is bypassed if the host name ends with the entry. Please note that you should list both the domain and the wildcard domain if you want to exclude a domain and all its subdomains. For example, you would set `NO_PROXY=example.com,.example.com` to bypass the proxy for requests sent to `http(s)://example.com` and `http(s)://subdomain.example.com`. ## Helm deployment @@ -23,7 +41,8 @@ upstream: value: '' - name: NO_PROXY # List of comma-separated URLs that should be excluded from proxying. - # Example: 'foo.com,baz.com' + # Make sure you include 'localhost'. + # Example: 'localhost,foo.com,baz.com' value: '' ``` @@ -62,7 +81,8 @@ spec: value: '' - name: NO_PROXY # List of comma-separated URLs that should be excluded from proxying. - # Example: 'foo.com,baz.com' + # Make sure you include 'localhost'. + # Example: 'localhost,foo.com,baz.com' value: '' ``` diff --git a/docs/proxy.md b/docs/proxy.md index f29c85911..db13227a6 100644 --- a/docs/proxy.md +++ b/docs/proxy.md @@ -2,7 +2,7 @@ As mentioned in [Running the Showcase application behind a corporate proxy](corporate-proxy.md), the `HTTP(S)_PROXY` and `NO_PROXY` environment variables are supported. -If you are behind a corporate proxy and are running the Showcase locally, as depicted in [Running locally with a basic configuration](index.md#running-locally-with-a-basic-configuration) or [Running locally with the Optional Plugins](index.md#running-locally-with-the-optional-plugins), you will need to additionally set the `GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE` to an empty value prior to running `yarn start`. +If you are behind a corporate proxy and are running the Showcase locally with `yarn`, as depicted in [Running locally with a basic configuration](index.md#running-locally-with-a-basic-configuration) or [Running locally with the Optional Plugins](index.md#running-locally-with-the-optional-plugins), you will need to additionally set the `GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE` to an empty value prior to running `yarn start`. Example: @@ -20,22 +20,117 @@ You can use the command below to quickly start a local corporate proxy server (b podman container run --rm --name squid-container \ -e TZ=UTC \ -p 3128:3128 \ - -it docker.io/ubuntu/squid:latest + -it registry.redhat.io/rhel9/squid:latest ``` # Plugin vendors The upstream Backstage project recommends the use of the `node-fetch` libraries in backend plugins for HTTP data fetching - see [ADR013](https://backstage.io/docs/architecture-decisions/adrs-adr013/). -We currently only support corporate proxy settings for Axios, `fetch` and `node-fetch` libraries. Backend plugins using any of these libraries have nothing special to do to support corporate proxies. +We currently only support corporate proxy settings for the Axios, `fetch` and `node-fetch` libraries. Backend plugins using any of these libraries have nothing special to do to support corporate proxies. + +Axios and `node-fetch` are supported with the proxy settings through the use of the [`global-agent`](https://github.com/gajus/global-agent#supported-libraries) package. +The native `fetch` library is supported with the proxy settings through the use of the Node's [`undici`](https://github.com/nodejs/undici) package. + +# Logging + +The following environment variables can be helpful when inspecting the behavior of the application with the proxy settings, for example to understand which requests are getting fetched through the proxy or directly bypassing the proxy. + +- `ROARR_LOG`: setting it to `true` enables the `global-agent` logs. More details in https://github.com/gajus/global-agent#enable-logging +- `NODE_DEBUG`: setting it to `fetch` or `undici` enables debug statements for the native `fetch` calls. More details in https://github.com/nodejs/undici/blob/main/docs/docs/api/Debug.md + +
+ +Example of logs + +We can get an output like below with the following environment variables set: + +- `NO_PROXY="localhost,.example.com"` +- `HTTP(S)_PROXY="http://proxy:3128"` +- `ROARR_LOG="true"` +- `NODE_DEBUG="fetch"` + +```text +[...] +{"context":{"package":"global-agent","namespace":"createGlobalProxyAgent","logLevel":30,"configuration":{"environmentVariableNamespace":"","forceGlobalAgent":true,"socketConnectionTimeout":60 +000}, + "state":{ + "HTTP_PROXY":"http://proxy:3128", + "HTTPS_PROXY":"http://proxy:3128", + "NO_PROXY":"localhost,.example.com"} + }, + "message":"global agent has been initialized", + "sequence":3,"time":1731063427425,"version":"1.0.0"} +[...] +{"context":{"package":"global-agent","namespace":"Agent","logLevel":10, + "destination":"https://api.github.com/user", + "proxy":"http://proxy:3128","requestId":2}, + "message":"proxying request", + "sequence":28,"time":1731063460170,"version":"1.0.0"} + +{"context":{"package":"global-agent","namespace":"Agent","logLevel":10, + "destination":"http://10.10.10.105:8888/path/path1.yaml", + "proxy":"http://proxy:3128","requestId":1}, + "message":"proxying request", + "sequence":23,"time":1731065107588,"version":"1.0.0"} + +[...] +{"context":{"package":"global-agent","namespace":"Agent","logLevel":10, + "destination":"http://localhost:7007/api/catalog/.backstage/auth/v1/jwks.json"}, + "message":"not proxying request; url matches GLOBAL_AGENT.NO_PROXY", + "sequence":4,"time":1731063026743,"version":"1.0.0"} + +{"context":{"package":"global-agent","namespace":"Agent","logLevel":10,"destination":"https://example.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml"}, + "message":"not proxying request; url matches GLOBAL_AGENT.NO_PROXY", + "sequence":5,"time":1731063027049,"version":"1.0.0"} + +{"context":{"package":"global-agent","namespace":"Agent","logLevel":10, + "destination":"https://example.com:1234/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml"}, + "message":"not proxying request; url matches GLOBAL_AGENT.NO_PROXY", + "sequence":6,"time":1731063027052,"version":"1.0.0"} + +{"context":{"package":"global-agent","namespace":"Agent","logLevel":10, + "destination":"https://subdomain.example.com/path/path1.yaml"}, + "message":"not proxying request; url matches GLOBAL_AGENT.NO_PROXY", + "sequence":7,"time":1731063027053,"version":"1.0.0"} + +{"context":{"package":"global-agent","namespace":"Agent","logLevel":10, + "destination":"https://subdomain2.example.com/path/path1.yaml"}, + "message":"not proxying request; url matches GLOBAL_AGENT.NO_PROXY", + "sequence":8,"time":1731063027055,"version":"1.0.0"} +[...] +FETCH 2: connecting to httpbin.org using https:undefined +FETCH 2: connecting to proxy:3128:3128 using http:undefined +FETCH 2: connected to proxy:3128:3128 using http:h1 +FETCH 2: sending request to CONNECT http://proxy:3128/httpbin.org:443 +FETCH 2: trailers received from GET https://httpbin.org//anything +FETCH 2: connected to proxy:3128:3128 using http:h1 +[...] +FETCH 16: connected to localhost:7007:7007 using http:h1 +FETCH 16: sending request to GET http://localhost:7007//api/catalog/.backstage/auth/v1/jwks.json +FETCH 16: received response to GET http://localhost:7007//api/catalog/.backstage/auth/v1/jwks.json - HTTP 200 +FETCH 16: trailers received from GET http://localhost:7007//api/catalog/.backstage/auth/v1/jwks.json +[...] +``` + +
+ # Testing The most challenging part of writing an end-to-end test from the context of a corporate proxy is to set up an environment where an application is forbidden access to the public Internet except through that proxy. -One possible approach is to simulate such an environment in a Kubernetes namespace with the help of [Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) to control ingress and egress traffic for pods within that namespace. +## Locally with `podman-compose` or `docker-compose` + +You can leverage the [`rhdh-local`](https://github.com/redhat-developer/rhdh-local) project to test how a given Showcase/Red Hat Developer Hub container image would behave when it is running behind a corporate proxy server. You would need either [`podman-compose`](https://docs.podman.io/en/latest/markdown/podman-compose.1.html) or [Docker Compose](https://docs.docker.com/compose/). + +See [Testing RHDH in a simulated corporate proxy setup](https://github.com/redhat-developer/rhdh-local#testing-rhdh-in-a-simulated-corporate-proxy-setup) for more details. + +## On a cluster -To do so: +This approach simulates a corporate proxy environment in a Kubernetes/OpenShift namespace with the help of [Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) to control ingress and egress traffic for pods within that namespace. + +### Kubernetes 1. Make sure the network plugin in your Kubernetes cluster supports network policies. [k3d](https://k3d.io) for example supports Network Policies out of the box. @@ -174,7 +269,7 @@ spec: # --- TRUNCATED --- ``` -# Testing on OpenShift +### OpenShift 2. Create a separate proxy project, and deploy a [Squid](https://www.squid-cache.org/)-based proxy application there. The full URL to access the proxy server from within the cluster would be `http://squid-service.proxy.svc.cluster.local:3128`. @@ -322,3 +417,16 @@ spec: - name: secrets-rhdh # --- TRUNCATED --- ``` + + + +# External Resources + +The way the proxy settings are supposed to be parsed and handled is unfortunately not codified in any standard, and might vary depending on the library, especially concerning the `NO_PROXY` handling. +See this nice article from GitLab highlighting some of the subtle differences that might cause issues: [We need to talk: Can we standardize NO_PROXY?](https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/). + +For reference, the following resources can help understand how the packages we use here handle such settings: +- Undici's proxy agent tests: https://github.com/nodejs/undici/blob/v6.19.8/test/env-http-proxy-agent.js +- global-agent tests: + - https://github.com/gajus/global-agent/blob/master/test/global-agent/factories/createGlobalProxyAgent.ts + - and specifically on the `NO_PROXY` handling: https://github.com/gajus/global-agent/blob/master/test/global-agent/utilities/isUrlMatchingNoProxy.ts diff --git a/packages/backend/src/corporate-proxy.ts b/packages/backend/src/corporate-proxy.ts index a205ee7df..ca58aca28 100644 --- a/packages/backend/src/corporate-proxy.ts +++ b/packages/backend/src/corporate-proxy.ts @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. import { bootstrap } from 'global-agent'; -import { Agent, Dispatcher, ProxyAgent, setGlobalDispatcher } from 'undici'; +import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici'; /** * Adds support for corporate proxy to both 'node-fetch' (using 'global-agent') and native 'fetch' (using 'undici') packages. * * Ref: https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/help-im-behind-a-corporate-proxy.md - * Ref: https://gist.github.com/zicklag/1bb50db6c5138de347c224fda14286da (to support 'no_proxy') */ export function configureCorporateProxyAgent() { // Bootstrap global-agent, which addresses node-fetch proxy-ing. @@ -28,76 +27,6 @@ export function configureCorporateProxyAgent() { // More details in https://github.com/gajus/global-agent#what-is-the-reason-global-agentbootstrap-does-not-use-http_proxy bootstrap(); - // Configure the undici package, which affects the native 'fetch'. It leverages the same env vars used by global-agent, - // or the more conventional HTTP(S)_PROXY ones. - const proxyEnv = - process.env.GLOBAL_AGENT_HTTP_PROXY ?? - process.env.GLOBAL_AGENT_HTTPS_PROXY ?? - process.env.HTTP_PROXY ?? - process.env.http_proxy ?? - process.env.HTTPS_PROXY ?? - process.env.https_proxy; - - if (proxyEnv) { - const proxyUrl = new URL(proxyEnv); - - // Create an access token if the proxy requires authentication - let token: string | undefined = undefined; - if (proxyUrl.username && proxyUrl.password) { - const b64 = Buffer.from( - `${proxyUrl.username}:${proxyUrl.password}`, - ).toString('base64'); - token = `Basic ${b64}`; - } - - // Create a default agent that will be used for no_proxy origins - const defaultAgent = new Agent(); - - // Create an interceptor that will use the appropriate agent based on the origin and the no_proxy - // environment variable. - // Collect the list of domains that we should not use a proxy for. - // The only wildcard available is a single * character, which matches all hosts, and effectively disables the proxy. - const noProxyEnv = - process.env.GLOBAL_AGENT_NO_PROXY ?? - process.env.NO_PROXY ?? - process.env.no_proxy; - const noProxyList = noProxyEnv?.split(',') || []; - - const isNoProxy = (origin?: string): boolean => { - for (const exclusion of noProxyList) { - if (exclusion === '*') { - // Effectively disables proxying - return true; - } - // Matched as either a domain which contains the hostname, or the hostname itself. - if (origin === exclusion || origin?.endsWith(`.${exclusion}`)) { - return true; - } - } - return false; - }; - - const noProxyInterceptor = ( - dispatch: Dispatcher['dispatch'], - ): Dispatcher['dispatch'] => { - return (opts, handler) => { - return isNoProxy(opts.origin?.toString()) - ? defaultAgent.dispatch(opts, handler) - : dispatch(opts, handler); - }; - }; - - // Create a proxy agent that will send all requests through the configured proxy, unless the - // noProxyInterceptor bypasses it. - const proxyAgent = new ProxyAgent({ - uri: proxyUrl.protocol + proxyUrl.host, - token, - interceptors: { - Client: [noProxyInterceptor], - }, - }); - - // Make sure our configured proxy agent is used for all `fetch()` requests globally. - setGlobalDispatcher(proxyAgent); - } + // Configure the undici package, which sets things up for the native 'fetch'. + setGlobalDispatcher(new EnvHttpProxyAgent()); } From 6f77c7db1b5fbeb2a647c2e1cab7f5cf11f75402 Mon Sep 17 00:00:00 2001 From: Christoph Jerolimov Date: Thu, 14 Nov 2024 09:32:36 +0100 Subject: [PATCH 3/3] chore(deps): update dynamic home page from mui v4 to v5 (#1828) Signed-off-by: Christoph Jerolimov --- plugins/dynamic-home-page/docs/headline.md | 7 +++--- plugins/dynamic-home-page/package.json | 6 +++-- .../src/components/Markdown.tsx | 6 ++--- .../src/components/Placeholder.tsx | 6 ++--- .../src/components/QuickAccessCard.tsx | 8 +++---- .../src/components/ReadOnlyGrid.tsx | 6 ++--- .../src/components/SearchBar.tsx | 22 +++++++++++-------- yarn.lock | 6 +++-- 8 files changed, 38 insertions(+), 29 deletions(-) diff --git a/plugins/dynamic-home-page/docs/headline.md b/plugins/dynamic-home-page/docs/headline.md index 98ca13061..b5d3e518c 100644 --- a/plugins/dynamic-home-page/docs/headline.md +++ b/plugins/dynamic-home-page/docs/headline.md @@ -27,6 +27,7 @@ dynamicPlugins: ## Available props -| Prop | Default | Description | -| ------- | ------- | ----------- | -| `title` | none | Title | +| Prop | Default | Description | +| ------- | ------- | ------------------------------------------ | +| `title` | none | Title | +| `align` | `left` | Alignment like `left`, `center` or `right` | diff --git a/plugins/dynamic-home-page/package.json b/plugins/dynamic-home-page/package.json index ea0dfbae4..02bab355b 100644 --- a/plugins/dynamic-home-page/package.json +++ b/plugins/dynamic-home-page/package.json @@ -41,10 +41,12 @@ "@backstage/plugin-search": "1.4.18", "@backstage/plugin-search-react": "1.8.1", "@backstage/theme": "0.6.0", - "@material-ui/core": "4.12.4", + "@mui/material": "5.16.7", + "@mui/styles": "5.16.7", "@scalprum/react-core": "0.8.0", "react-grid-layout": "1.4.4", - "react-use": "17.5.1" + "react-use": "17.5.1", + "tss-react": "4.9.12" }, "peerDependencies": { "react": "16.13.1 || ^17.0.0 || ^18.2.0", diff --git a/plugins/dynamic-home-page/src/components/Markdown.tsx b/plugins/dynamic-home-page/src/components/Markdown.tsx index a35ae49df..8215b70e8 100644 --- a/plugins/dynamic-home-page/src/components/Markdown.tsx +++ b/plugins/dynamic-home-page/src/components/Markdown.tsx @@ -1,13 +1,13 @@ import { MarkdownContent } from '@backstage/core-components'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from 'tss-react/mui'; export interface MarkdownProps { title?: string; content?: string; } -const useStyles = makeStyles({ +const useStyles = makeStyles()({ // Make card content scrollable (so that cards don't overlap) card: { display: 'flex', @@ -21,7 +21,7 @@ const useStyles = makeStyles({ }); export const Markdown = (props: MarkdownProps) => { - const classes = useStyles(); + const { classes } = useStyles(); return (
{props.title ?

{props.title}

: null} diff --git a/plugins/dynamic-home-page/src/components/Placeholder.tsx b/plugins/dynamic-home-page/src/components/Placeholder.tsx index c17c126c6..a8ee56a0b 100644 --- a/plugins/dynamic-home-page/src/components/Placeholder.tsx +++ b/plugins/dynamic-home-page/src/components/Placeholder.tsx @@ -1,11 +1,11 @@ -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from 'tss-react/mui'; export interface PlaceholderProps { showBorder?: boolean; debugContent?: string; } -const useStyles = makeStyles({ +const useStyles = makeStyles()({ centerDebugContent: { height: '100%', display: 'flex', @@ -21,7 +21,7 @@ const useStyles = makeStyles({ }); export const Placeholder = (props: PlaceholderProps) => { - const classes = useStyles(); + const { classes } = useStyles(); const className = [ props.debugContent ? classes.centerDebugContent : undefined, props.showBorder ? classes.showBorder : undefined, diff --git a/plugins/dynamic-home-page/src/components/QuickAccessCard.tsx b/plugins/dynamic-home-page/src/components/QuickAccessCard.tsx index 64af1a33d..34e6a4a26 100644 --- a/plugins/dynamic-home-page/src/components/QuickAccessCard.tsx +++ b/plugins/dynamic-home-page/src/components/QuickAccessCard.tsx @@ -5,12 +5,12 @@ import { } from '@backstage/core-components'; import { ComponentAccordion, HomePageToolkit } from '@backstage/plugin-home'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import { makeStyles } from '@material-ui/core/styles'; +import CircularProgress from '@mui/material/CircularProgress'; +import { makeStyles } from 'tss-react/mui'; import { useQuickAccessLinks } from '../hooks/useQuickAccessLinks'; -const useStyles = makeStyles({ +const useStyles = makeStyles()({ center: { height: '100%', display: 'flex', @@ -34,7 +34,7 @@ export interface QuickAccessCardProps { } export const QuickAccessCard = (props: QuickAccessCardProps) => { - const classes = useStyles(); + const { classes } = useStyles(); const { data, error, isLoading } = useQuickAccessLinks(props.path); let content: React.ReactElement; diff --git a/plugins/dynamic-home-page/src/components/ReadOnlyGrid.tsx b/plugins/dynamic-home-page/src/components/ReadOnlyGrid.tsx index cd95fc3f1..acd7258ae 100644 --- a/plugins/dynamic-home-page/src/components/ReadOnlyGrid.tsx +++ b/plugins/dynamic-home-page/src/components/ReadOnlyGrid.tsx @@ -14,7 +14,7 @@ import { import { ErrorBoundary } from '@backstage/core-components'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from 'tss-react/mui'; // Removes the doubled scrollbar import 'react-grid-layout/css/styles.css'; @@ -62,7 +62,7 @@ const defaultProps: ResponsiveProps = { compactType: null, }; -const useStyles = makeStyles({ +const useStyles = makeStyles()({ // Make card content scrollable (so that cards don't overlap) cardWrapper: { '& > div[class*="MuiCard-root"]': { @@ -82,7 +82,7 @@ export interface ReadOnlyGridProps { } export const ReadOnlyGrid = (props: ReadOnlyGridProps) => { - const classes = useStyles(); + const { classes } = useStyles(); const [measureRef, measureRect] = useMeasure(); const cards = useMemo(() => { diff --git a/plugins/dynamic-home-page/src/components/SearchBar.tsx b/plugins/dynamic-home-page/src/components/SearchBar.tsx index e22e3ff3d..392f8383c 100644 --- a/plugins/dynamic-home-page/src/components/SearchBar.tsx +++ b/plugins/dynamic-home-page/src/components/SearchBar.tsx @@ -3,18 +3,22 @@ import { useNavigate } from 'react-router-dom'; import { SearchBarBase } from '@backstage/plugin-search-react'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from 'tss-react/mui'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles()(theme => ({ searchBar: { - backgroundColor: theme.palette.type === 'dark' ? '#36373A' : '#FFFFFF', - boxShadow: 'none', - border: `1px solid ${theme.palette.type === 'dark' ? '#57585a' : '#E4E4E4'}`, - borderRadius: '50px', - margin: 0, + '&&': { + backgroundColor: theme.palette.mode === 'dark' ? '#36373A' : '#FFFFFF', + boxShadow: 'none', + border: `1px solid ${theme.palette.mode === 'dark' ? '#57585a' : '#E4E4E4'}`, + borderRadius: '50px', + margin: 0, + }, }, notchedOutline: { - borderStyle: 'none !important', + '&&': { + borderStyle: 'none', + }, }, })); @@ -24,7 +28,7 @@ export interface SearchBarProps { } export const SearchBar = ({ path, queryParam }: SearchBarProps) => { - const classes = useStyles(); + const { classes } = useStyles(); const [value, setValue] = useState(''); const ref = useRef(null); const navigate = useNavigate(); diff --git a/yarn.lock b/yarn.lock index 6ded596e8..aa81154c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9521,7 +9521,8 @@ __metadata: "@backstage/plugin-search-react": 1.8.1 "@backstage/test-utils": 1.7.0 "@backstage/theme": 0.6.0 - "@material-ui/core": 4.12.4 + "@mui/material": 5.16.7 + "@mui/styles": 5.16.7 "@openshift/dynamic-plugin-sdk": 5.0.1 "@redhat-developer/red-hat-developer-hub-theme": 0.4.0 "@scalprum/react-core": 0.8.0 @@ -9533,6 +9534,7 @@ __metadata: prettier: 3.3.3 react-grid-layout: 1.4.4 react-use: 17.5.1 + tss-react: 4.9.12 peerDependencies: react: 16.13.1 || ^17.0.0 || ^18.2.0 react-router-dom: 6.26.2 @@ -10830,7 +10832,7 @@ __metadata: languageName: node linkType: hard -"@material-ui/core@npm:4.12.4, @material-ui/core@npm:^4.12.2, @material-ui/core@npm:^4.12.4, @material-ui/core@npm:^4.9.13": +"@material-ui/core@npm:^4.12.2, @material-ui/core@npm:^4.12.4, @material-ui/core@npm:^4.9.13": version: 4.12.4 resolution: "@material-ui/core@npm:4.12.4" dependencies: