Skip to content
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
2 changes: 1 addition & 1 deletion ui/apps/platform/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<title></title>
</head>

<body class="theme-light">
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
Expand Down
7 changes: 7 additions & 0 deletions ui/apps/platform/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/apps/platform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@apollo/client": "^3.6.3",
"@lifeomic/axios-fetch": "^3.1.0",
"@openshift-console/dynamic-plugin-sdk": "4.19.0",
"@patternfly/patternfly": "^6.4.0",
"@patternfly/react-charts": "^8.4.1",
"@patternfly/react-component-groups": "^6.4.0",
"@patternfly/react-core": "^6.4.1",
Expand Down
44 changes: 1 addition & 43 deletions ui/apps/platform/src/Components/CodeViewer.cy.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import CodeViewer, { CodeViewerThemeProvider } from './CodeViewer';
import CodeViewer from './CodeViewer';

const sampleYaml = `apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
Expand Down Expand Up @@ -37,46 +37,4 @@ describe(Cypress.spec.relative, () => {
});
});
});

it('should toggle light and dark editor themes', () => {
cy.mount(<CodeViewer code={sampleYaml} />);

// Default to light mode
cy.get('[data-component-theme="light"]').should('exist');
cy.get('[data-component-theme="dark"]').should('not.exist');

cy.get('button[aria-label="Set dark theme"]').click();
cy.get('[data-component-theme="light"]').should('not.exist');
cy.get('[data-component-theme="dark"]').should('exist');

cy.get('button[aria-label="Set light theme"]').click();
cy.get('[data-component-theme="light"]').should('exist');
cy.get('[data-component-theme="dark"]').should('not.exist');
});

it('should share theme state across multiple instances', () => {
cy.mount(
<CodeViewerThemeProvider>
<CodeViewer code={sampleYaml} />
<CodeViewer code={sampleYaml} />
</CodeViewerThemeProvider>
);

cy.get('[data-component-theme="light"]').should('have.length', 2);
cy.get('[data-component-theme="dark"]').should('have.length', 0);

cy.get('button[aria-label="Set dark theme"]').eq(0);
cy.get('button[aria-label="Set dark theme"]').eq(1);
cy.get('button[aria-label="Set dark theme"]').eq(0).click();

cy.get('[data-component-theme="light"]').should('have.length', 0);
cy.get('[data-component-theme="dark"]').should('have.length', 2);

cy.get('button[aria-label="Set light theme"]').eq(0);
cy.get('button[aria-label="Set light theme"]').eq(1);
cy.get('button[aria-label="Set light theme"]').eq(1).click();

cy.get('[data-component-theme="light"]').should('have.length', 2);
cy.get('[data-component-theme="dark"]').should('have.length', 0);
});
});
67 changes: 6 additions & 61 deletions ui/apps/platform/src/Components/CodeViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,19 @@
import { createContext, useContext, useState } from 'react';
import type { CSSProperties, Dispatch, ReactElement, ReactNode, SetStateAction } from 'react';
import { Button, ClipboardCopyButton, CodeBlock, CodeBlockAction } from '@patternfly/react-core';
import { MoonIcon, SunIcon } from '@patternfly/react-icons';
import type { CSSProperties, ReactElement, ReactNode } from 'react';
import { ClipboardCopyButton, CodeBlock, CodeBlockAction } from '@patternfly/react-core';

import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import lightTheme from 'react-syntax-highlighter/dist/esm/styles/prism/one-light';
import darkTheme from 'react-syntax-highlighter/dist/esm/styles/prism/one-dark';
import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml';

import useClipboardCopy from 'hooks/useClipboardCopy';
import { useTheme } from 'hooks/useTheme';

SyntaxHighlighter.registerLanguage('yaml', yaml);

const CodeViewerThemeContext = createContext<
['light' | 'dark', Dispatch<SetStateAction<'light' | 'dark'>>] | undefined
>(undefined);

export const CodeViewerThemeProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<'light' | 'dark'>('light');

return (
<CodeViewerThemeContext.Provider value={[state, setState]}>
{children}
</CodeViewerThemeContext.Provider>
);
};

export const useCodeViewerThemeContext = () => {
const context = useContext(CodeViewerThemeContext);
// Fallback state provides the ability to toggle theme for a single instance if no provider is detected uptree
const fallbackState = useState<'light' | 'dark'>('light');
return context ?? fallbackState;
};

// When adding to the supported languages, the correct language definition must be imported and registered as well
type SupportedLanguages = 'yaml';

const defaultStyle = {
'--pf-v6-u-max-height--MaxHeight': '300px',
'--pf-v6-c-code-block__content--PaddingBlockStart': '0',
'--pf-v6-c-code-block__content--PaddingBlockEnd': '0',
'--pf-v6-c-code-block__content--PaddingInlineStart': '0',
'--pf-v6-c-code-block__content--PaddingInlineEnd': '0',
overflowY: 'auto',
} as const;

const lightThemeStyles = {} as const;

// TODO This should be deleted when we move to proper PatternFly theming
const darkThemeStyles = {
'--pf-t--global--background--color--secondary--default': 'var(--pf-t--color--gray--95)',
'--pf-t--global--text--color--regular': 'var(--pf-t--color--white)',
'--pf-t--global--icon--color--regular': 'var(--pf-t--color--white)',
} as const;

export type CodeViewerProps = {
code: string;
language?: SupportedLanguages;
Expand All @@ -70,11 +30,7 @@ export default function CodeViewer({
additionalControls,
}: CodeViewerProps): ReactElement {
const { wasCopied, setWasCopied, copyToClipboard } = useClipboardCopy();
const [theme, setTheme] = useCodeViewerThemeContext();

function toggleTheme() {
setTheme((prevValue) => (prevValue === 'light' ? 'dark' : 'light'));
}
const theme = useTheme();

const actions = (
<>
Expand All @@ -90,33 +46,22 @@ export default function CodeViewer({
{wasCopied ? 'Successfully copied to clipboard!' : 'Copy to clipboard'}
</ClipboardCopyButton>
</CodeBlockAction>
<CodeBlockAction>
<Button
variant="plain"
aria-label={theme === 'light' ? 'Set dark theme' : 'Set light theme'}
icon={theme === 'light' ? <MoonIcon /> : <SunIcon />}
onClick={() => toggleTheme()}
/>
</CodeBlockAction>
{additionalControls}
</>
);

const themeStyles = theme === 'light' ? lightThemeStyles : darkThemeStyles;

// TODO - When Tailwind is removed, we likely need to get rid of this font size override
return (
<CodeBlock
data-component-theme={theme}
className={`pf-v6-u-p-0 pf-v6-u-font-size-xs pf-v6-u-max-height ${className}`}
style={{ ...defaultStyle, ...style, ...themeStyles }}
style={style}
actions={actions}
>
<SyntaxHighlighter
language={language}
showLineNumbers
wrapLongLines
style={theme === 'light' ? lightTheme : darkTheme}
style={theme.isDarkMode ? darkTheme : lightTheme}
customStyle={{
margin: 0,
background: 'var(--pf-v6-c-code-block--BackgroundColor)',
Expand Down
12 changes: 8 additions & 4 deletions ui/apps/platform/src/Components/PatternFly/BrandLogo.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { ReactElement } from 'react';
import { Brand } from '@patternfly/react-core';
import type { CSSProperties, ReactElement } from 'react';
import { getProductBranding } from 'constants/productBranding';

export type BrandLogoProps = {
style?: CSSProperties;
className?: string;
};

function BrandLogo(props: BrandLogoProps): ReactElement {
function BrandLogo({ style, className }: BrandLogoProps): ReactElement {
const branding = getProductBranding();
return <Brand {...props} src={branding.logoSvg} alt={branding.logoAltText} />;
const { LogoComponent, logoAltText } = branding;

return (
<LogoComponent style={style} className={className} aria-label={logoAltText} role="img" />
);
}

export default BrandLogo;
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,16 @@ import searchContext from 'Containers/searchContext';
import workflowStateContext from 'Containers/workflowStateContext';
import { newWorkflowCases } from 'constants/useCaseTypes';

const backgroundClass = 'bg-base-100';
const borderClass = 'border border-base-300';
const colorClass = 'pf-v6-u-color-100'; // override color from React select style rules
const categoryOptionStyles = {
backgroundColor: 'var(--pf-t--global--color--nonstatus--blue--default)',
};
const categoryOptionClass = `pf-v6-u-font-weight-bold ${borderClass} ${colorClass}`;
const valueOptionClass = `${backgroundClass} ${borderClass} ${colorClass}`;
const categoryClass = `pf-v6-u-font-weight-bold text-base-800 bg-primary-400 border border-base-300`;
const valueClass = `text-base-800 bg-base-100 border border-base-300`;

// Render readonly input with placeholder instead of span to prevent insufficient color contrast.
export const placeholderCreator = (placeholderText) =>
function Placeholder() {
return (
<span className="flex h-full items-center pointer-events-none">
<input
className={`${backgroundClass} ${colorClass} absolute pf-v6-u-w-100`}
className={`bg-base-100 absolute pf-v6-u-w-100`}
placeholder={placeholderText}
readOnly
/>
Expand All @@ -43,8 +37,7 @@ export const Option = ({ children, ...rest }) => {
<components.Option {...rest}>
<div className="flex">
<span
style={isCategoryChip(children) ? categoryOptionStyles : {}}
className={`${isCategoryChip(children) ? categoryOptionClass : valueOptionClass} rounded-sm p-1 px-2 text-sm`}
className={`${isCategoryChip(children) ? categoryClass : valueClass} rounded-sm p-1 px-2 text-sm`}
>
{children}
</span>
Expand All @@ -66,7 +59,7 @@ export const MultiValue = (props) => (
<components.MultiValue
{...props}
className={`${
props.data.type === 'categoryOption' ? categoryOptionClass : valueOptionClass
props.data.type === 'categoryOption' ? categoryClass : valueClass
} ${props.data.ignore ? 'hidden' : ''}`}
/>
);
Expand Down
3 changes: 3 additions & 0 deletions ui/apps/platform/src/Containers/AppPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import LoginPage from 'Containers/Login/LoginPage';
import TestLoginResultsPage from 'Containers/Login/TestLoginResultsPage';
import AppPageTitle from 'Containers/AppPageTitle';
import AppPageFavicon from 'Containers/AppPageFavicon';
import { useTheme } from 'hooks/useTheme';

function AppPage(): ReactElement {
useTheme();

return (
<>
<AppPageTitle />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { ReactElement } from 'react';
import { Tooltip } from '@patternfly/react-core';

import helm from 'images/helm.svg';
import HelmLogo from 'images/helm.svg?react';

function HelmIndicator(): ReactElement {
return (
<Tooltip content="This cluster is managed by Helm.">
<span className="w-5 h-5 inline-block pf-v6-u-flex-shrink-0">
<img className="w-5 h-5" src={helm} alt="Managed by Helm" />
<HelmLogo className="w-5 h-5" />
</span>
</Tooltip>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import {
} from '../utils/integrationsList';
import IntegrationTile from './IntegrationTile';

const { image, label, type } = descriptor;
const { Logo, label, type } = descriptor;

function APITokensTile(): ReactElement {
const { data } = useRestQuery(fetchAPITokens);
const integrations = data?.response?.tokens ?? [];

return (
<IntegrationTile
image={image}
Logo={Logo}
label={label}
linkTo={getIntegrationsListPath(source, type)}
numIntegrations={integrations.length}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ function BackupIntegrationsTab({ sourcesEnabled }: IntegrationsTabProps): ReactE
)}
<Gallery hasGutter>
{descriptors.filter(featureFlagDependencyFilter).map((descriptor) => {
const { image, label, type } = descriptor;
const { Logo, label, type } = descriptor;

return (
<IntegrationTile
key={type}
image={image}
Logo={Logo}
label={label}
linkTo={getIntegrationsListPath(source, type)}
numIntegrations={countIntegrations(type)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ function ImageIntegrationsTab({ sourcesEnabled }: IntegrationsTabProps): ReactEl
)}
<Gallery hasGutter>
{descriptors.filter(featureFlagDependencyFilter).map((descriptor) => {
const { categories, image, label, type } = descriptor;
const { categories, Logo, label, type } = descriptor;

return (
<IntegrationTile
key={type}
categories={categories}
image={image}
Logo={Logo}
label={label}
linkTo={getIntegrationsListPath(source, type)}
numIntegrations={countIntegrations(type)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactElement } from 'react';
import type { ComponentType, ReactElement, SVGProps } from 'react';
import {
Badge,
Card,
Expand All @@ -15,7 +15,7 @@ import TechPreviewLabel from 'Components/PatternFly/PreviewLabel/TechPreviewLabe

type IntegrationTileProps = {
categories?: string;
image: string;
Logo: ComponentType<SVGProps<SVGSVGElement>>;
label: string;
linkTo: string;
numIntegrations: number;
Expand All @@ -24,7 +24,7 @@ type IntegrationTileProps = {

function IntegrationTile({
categories,
image,
Logo,
label,
linkTo,
numIntegrations,
Expand All @@ -48,7 +48,11 @@ function IntegrationTile({
{numIntegrations}
</Badge>
)}
<img src={image} alt="" style={{ height: '100px' }} />
<Logo
aria-label="Integration logo"
role="img"
style={{ height: '100px', width: 'auto', maxWidth: '100%' }}
/>
</>
</CardHeader>
<CardTitle className="pf-v6-u-color-100" style={{ whiteSpace: 'nowrap' }}>
Expand Down
Loading
Loading