Skip to content

Commit a691e64

Browse files
Merge pull request #15254 from logonoff/CONSOLE-4499-codeeditor-electric-boogaloo
CONSOLE-4656, CONSOLE-4499, CONSOLE-4654, CONSOLE-4657: Implement some CodeEditor RFEs
2 parents 869f77e + 0eb4740 commit a691e64

17 files changed

Lines changed: 530 additions & 201 deletions

File tree

frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,8 +659,10 @@ export type CodeEditorToolbarProps = {
659659
// Omit the ref as we have our own ref type, which is completely different
660660
export type BasicCodeEditorProps = Partial<Omit<PfCodeEditorProps, 'ref'>>;
661661

662-
export type CodeEditorProps = Omit<BasicCodeEditorProps, 'code'> &
662+
export type CodeEditorProps = Omit<BasicCodeEditorProps, 'code' | 'shortcutsPopoverProps'> &
663663
CodeEditorToolbarProps & {
664+
/** Additional props to override the default popover properties */
665+
shortcutsPopoverProps?: Partial<PfCodeEditorProps['shortcutsPopoverProps']>;
664666
/** Code displayed in code editor. */
665667
value?: string;
666668
/** Minimum editor height in valid CSS height values. */

frontend/packages/console-shared/locales/en/console-shared.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@
124124
"View document outline": "View document outline",
125125
"View property descriptions": "View property descriptions",
126126
"Save": "Save",
127+
"Hide sidebar": "Hide sidebar",
128+
"Show sidebar": "Show sidebar",
127129
"Restricted access": "Restricted access",
128130
"You don't have access to this section due to cluster policy": "You don't have access to this section due to cluster policy",
129131
"Error details": "Error details",

frontend/packages/console-shared/src/components/editor/CodeEditor.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>((props, ref)
1515
const [monacoRef, setMonacoRef] = React.useState<CodeEditorRef['monaco'] | null>(null);
1616
const [usesValue] = React.useState<boolean>(value !== undefined);
1717

18-
const shortcutPopover = useShortcutPopover();
18+
const shortcutPopover = useShortcutPopover(props.shortcutsPopoverProps);
1919

2020
const editorDidMount: EditorDidMount = React.useCallback(
2121
(editor, monaco) => {
@@ -55,7 +55,7 @@ const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>((props, ref)
5555
// do not render toolbar if the component is null
5656
const ToolbarLinks = React.useMemo(() => {
5757
return showShortcuts || toolbarLinks?.length ? (
58-
<CodeEditorToolbar toolbarLinks={toolbarLinks} showShortcuts={showShortcuts} />
58+
<CodeEditorToolbar toolbarLinks={toolbarLinks} />
5959
) : undefined;
6060
}, [toolbarLinks, showShortcuts]);
6161

@@ -80,11 +80,11 @@ const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>((props, ref)
8080
<div style={{ minHeight }} className="ocs-yaml-editor">
8181
<BasicCodeEditor
8282
{...props}
83-
language={props?.language ?? Language.yaml}
83+
language={props.language ?? Language.yaml}
8484
code={value}
85-
options={{ ...defaultEditorOptions, ...props?.options }}
85+
options={{ ...defaultEditorOptions, ...props.options }}
8686
onEditorDidMount={editorDidMount}
87-
isFullHeight={props?.isFullHeight ?? true}
87+
isFullHeight={props.isFullHeight ?? true}
8888
customControls={ToolbarLinks ?? undefined}
8989
shortcutsPopoverProps={showShortcuts ? shortcutPopover : undefined}
9090
/>

frontend/packages/console-shared/src/components/editor/CodeEditorToolbar.tsx

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,40 @@
1-
import * as React from 'react';
2-
import { Button, Flex, FlexItem } from '@patternfly/react-core';
1+
import { Button, Tooltip } from '@patternfly/react-core';
32
import { MagicIcon } from '@patternfly/react-icons';
43
import { useTranslation } from 'react-i18next';
54
import { useDispatch } from 'react-redux';
65
import { action } from 'typesafe-actions';
76
import { CodeEditorToolbarProps } from '@console/dynamic-plugin-sdk';
87
import { ActionType } from '@console/internal/reducers/ols';
9-
import { useOLSConfig } from '../../hooks/ols-hook';
8+
import { useOLSConfig } from '@console/shared/src/hooks/ols-hook';
9+
import { useIsFullscreen } from '@console/shared/src/hooks/useFullscreen';
1010

11-
export const AskOpenShiftLightspeedButton: React.FC = () => {
11+
export const AskOpenShiftLightspeedButton: React.FCC = () => {
1212
const { t } = useTranslation('console-shared');
1313
const openOLS = () => action(ActionType.OpenOLS);
1414
const showLightspeedButton = useOLSConfig();
1515
const dispatch = useDispatch();
16+
const isFullscreen = useIsFullscreen();
1617

1718
return showLightspeedButton ? (
18-
<Button
19-
variant="secondary"
20-
size="sm"
21-
onClick={() => {
22-
dispatch(openOLS());
23-
}}
24-
icon={<MagicIcon />}
25-
>
26-
{t('console-shared~Ask OpenShift Lightspeed')}
27-
</Button>
19+
<Tooltip content={t('Ask OpenShift Lightspeed')}>
20+
<Button
21+
isDisabled={isFullscreen}
22+
variant="plain"
23+
onClick={() => dispatch(openOLS())}
24+
aria-label={t('Ask OpenShift Lightspeed')}
25+
icon={<MagicIcon />}
26+
/>
27+
</Tooltip>
2828
) : null;
2929
};
3030

31-
export const CodeEditorToolbar: React.FC<CodeEditorToolbarProps> = ({
32-
showShortcuts,
33-
toolbarLinks,
34-
}) => {
35-
if (!showShortcuts && !toolbarLinks?.length) return null;
31+
export const CodeEditorToolbar: React.FCC<CodeEditorToolbarProps> = ({ toolbarLinks }) => {
32+
if (!toolbarLinks?.length) return null;
33+
3634
return (
3735
<>
3836
<AskOpenShiftLightspeedButton />
39-
40-
<Flex className="pf-v6-u-ml-xs" alignItems={{ default: 'alignItemsCenter' }}>
41-
{toolbarLinks &&
42-
toolbarLinks.map((link, index) => (
43-
// eslint-disable-next-line react/no-array-index-key
44-
<FlexItem key={`${index}`}>{link}</FlexItem>
45-
))}
46-
</Flex>
37+
{toolbarLinks}
4738
</>
4839
);
4940
};
Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
1+
import { useMemo } from 'react';
12
import { PopoverProps } from '@patternfly/react-core';
23
import { useTranslation } from 'react-i18next';
34
import { ShortcutTable, Shortcut } from '../shortcuts';
45
import { isMac } from '../shortcuts/Shortcut';
56

6-
export const useShortcutPopover = (onHideShortcuts?: () => {}): PopoverProps => {
7+
export const useShortcutPopover = (shortcutsPopoverProps?: Partial<PopoverProps>): PopoverProps => {
78
const { t } = useTranslation('console-shared');
89

9-
return {
10-
'aria-label': t('Shortcuts'),
11-
bodyContent: (
12-
<ShortcutTable>
13-
<Shortcut keyName="F1">{t('View all editor shortcuts')}</Shortcut>
14-
<Shortcut ctrl keyName="space">
15-
{t('Activate auto complete')}
16-
</Shortcut>
17-
<Shortcut ctrl shift={isMac} keyName="m">
18-
{t('Toggle Tab action between insert Tab character and move focus out of editor')}
19-
</Shortcut>
20-
<Shortcut ctrlCmd shift keyName="o">
21-
{t('View document outline')}
22-
</Shortcut>
23-
<Shortcut hover>{t('View property descriptions')}</Shortcut>
24-
<Shortcut ctrlCmd keyName="s">
25-
{t('Save')}
26-
</Shortcut>
27-
</ShortcutTable>
28-
),
29-
maxWidth: '25rem',
30-
distance: 18,
31-
onHide: onHideShortcuts,
32-
};
10+
return useMemo((): PopoverProps => {
11+
return {
12+
'aria-label': t('Shortcuts'),
13+
bodyContent: (
14+
<ShortcutTable>
15+
<Shortcut keyName="F1">{t('View all editor shortcuts')}</Shortcut>
16+
<Shortcut ctrl keyName="space">
17+
{t('Activate auto complete')}
18+
</Shortcut>
19+
<Shortcut ctrl shift={isMac} keyName="m">
20+
{t('Toggle Tab action between insert Tab character and move focus out of editor')}
21+
</Shortcut>
22+
<Shortcut ctrlCmd shift keyName="o">
23+
{t('View document outline')}
24+
</Shortcut>
25+
<Shortcut hover>{t('View property descriptions')}</Shortcut>
26+
<Shortcut ctrlCmd keyName="s">
27+
{t('Save')}
28+
</Shortcut>
29+
</ShortcutTable>
30+
),
31+
maxWidth: '25rem',
32+
distance: 18,
33+
...shortcutsPopoverProps,
34+
};
35+
}, [t, shortcutsPopoverProps]);
3336
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { CodeEditorControl, CodeEditorControlProps } from '@patternfly/react-code-editor';
2+
import { createIcon } from '@patternfly/react-icons/dist/esm/createIcon';
3+
import { useTranslation } from 'react-i18next';
4+
5+
export const SidebarOffIcon = createIcon({
6+
name: 'SidebarOffIcon',
7+
width: 512,
8+
height: 512,
9+
svgPath:
10+
'M463.98 31.958H48.02C21.5 31.958 0 53.458 0 79.98v352.042c0 26.52 21.5 48.02 48.02 48.02h415.96c26.52 0 48.02-21.5 48.02-48.02V79.979c0-26.52-21.5-48.02-48.02-48.02zm-239.938 384H64V96.042h159.958v320ZM448 315.398v100.56H288.042V96.042H448zm-29.867-78.473h-98.3a8.426 8.426 0 0 0-8.45 8.45v21.25a8.426 8.426 0 0 0 8.45 8.45h98.3a8.426 8.426 0 0 0 8.45-8.45v-21.25a8.426 8.426 0 0 0-8.45-8.45zm0-84.83h-98.3a8.426 8.426 0 0 0-8.45 8.449v21.25a8.426 8.426 0 0 0 8.45 8.45h98.3a8.426 8.426 0 0 0 8.45-8.45v-21.25a8.426 8.426 0 0 0-8.45-8.45zm0 169.662h-98.3a8.426 8.426 0 0 0-8.45 8.45v21.25a8.426 8.426 0 0 0 8.45 8.449h98.3a8.426 8.426 0 0 0 8.45-8.45v-21.25a8.426 8.426 0 0 0-8.45-8.45z',
11+
});
12+
13+
export const SidebarOnIcon = createIcon({
14+
name: 'SidebarOnIcon',
15+
width: 512,
16+
height: 512,
17+
svgPath:
18+
'M463.98 31.958H48.02C21.5 31.958 0 53.458 0 79.98v352.042c0 26.52 21.5 48.02 48.02 48.02h415.96c26.52 0 48.02-21.5 48.02-48.02V79.979c0-26.52-21.5-48.02-48.02-48.02zm-239.938 384H64V96.042h159.958v320ZM448 315.398v100.56H288.042V96.042H448zM248.638 66.929v369.443h214.84V66.928Zm177.945 284.528a8.426 8.426 0 0 1-8.45 8.45h-98.3a8.426 8.426 0 0 1-8.45-8.45v-21.25a8.426 8.426 0 0 1 8.45-8.45h98.3a8.426 8.426 0 0 1 8.45 8.45zm0-84.831a8.426 8.426 0 0 1-8.45 8.45h-98.3a8.426 8.426 0 0 1-8.45-8.45v-21.25a8.426 8.426 0 0 1 8.45-8.45h98.3a8.426 8.426 0 0 1 8.45 8.45zm0-84.832a8.426 8.426 0 0 1-8.45 8.45h-98.3a8.426 8.426 0 0 1-8.45-8.45v-21.25a8.426 8.426 0 0 1 8.45-8.449h98.3a8.426 8.426 0 0 1 8.45 8.45z',
19+
});
20+
21+
interface ToggleSidebarButtonProps extends Partial<CodeEditorControlProps> {
22+
isSidebarOpen: boolean;
23+
toggleSidebar: () => void;
24+
/** Adds a div with `flex-grow: 1` so that the button is aligned to the end of the toolbar */
25+
alignToEnd?: boolean;
26+
}
27+
28+
export const ToggleSidebarButton: React.FCC<ToggleSidebarButtonProps> = ({
29+
isSidebarOpen,
30+
toggleSidebar,
31+
alignToEnd = false,
32+
...props
33+
}) => {
34+
const { t } = useTranslation('console-shared');
35+
36+
return (
37+
<>
38+
{alignToEnd && <div style={{ flexGrow: 1 }} />}
39+
<CodeEditorControl
40+
aria-label={isSidebarOpen ? t('Hide sidebar') : t('Show sidebar')}
41+
onClick={toggleSidebar}
42+
icon={isSidebarOpen ? <SidebarOnIcon /> : <SidebarOffIcon />}
43+
tooltipProps={{ content: isSidebarOpen ? t('Hide sidebar') : t('Show sidebar') }}
44+
{...props}
45+
/>
46+
</>
47+
);
48+
};
Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Button } from '@patternfly/react-core';
2-
import { shallow, ShallowWrapper } from 'enzyme';
1+
import { render, screen, fireEvent } from '@testing-library/react';
32
import { useTranslation } from 'react-i18next';
43
import { useDispatch } from 'react-redux';
54
import { ActionType } from '@console/internal/reducers/ols';
65
import { useOLSConfig } from '../../../hooks/ols-hook';
76
import { AskOpenShiftLightspeedButton, CodeEditorToolbar } from '../CodeEditorToolbar';
7+
import '@testing-library/jest-dom';
88

99
jest.mock('react-i18next', () => ({
1010
useTranslation: jest.fn(),
@@ -19,7 +19,6 @@ jest.mock('../../../hooks/ols-hook', () => ({
1919
}));
2020

2121
describe('CodeEditorToolbar', () => {
22-
let wrapper: ShallowWrapper;
2322
const mockDispatch = jest.fn();
2423

2524
beforeEach(() => {
@@ -29,32 +28,32 @@ describe('CodeEditorToolbar', () => {
2928
});
3029

3130
it('should render null when showShortcuts is false and toolbarLinks is empty', () => {
32-
wrapper = shallow(<CodeEditorToolbar />);
33-
expect(wrapper.isEmptyRender()).toBe(true);
31+
const { container } = render(<CodeEditorToolbar />);
32+
expect(container.firstChild).toBeNull();
3433
});
3534

3635
it('should render toolbar with custom links when toolbarLinks are provided', () => {
37-
const toolbarLinks = [<div key="custom">Custom Link</div>];
38-
wrapper = shallow(<CodeEditorToolbar toolbarLinks={toolbarLinks} />);
39-
expect(wrapper.contains(<div>Custom Link</div>)).toBe(true);
36+
render(<CodeEditorToolbar toolbarLinks={[<div key="custom">Custom Link</div>]} />);
37+
expect(screen.getByText('Custom Link')).toBeInTheDocument();
4038
});
4139

4240
it('should render "Ask OpenShift Lightspeed" button when showLightspeedButton is true', () => {
4341
(useOLSConfig as jest.Mock).mockReturnValue(true);
44-
wrapper = shallow(<AskOpenShiftLightspeedButton />);
45-
expect(wrapper.find(Button).prop('children')).toBe('console-shared~Ask OpenShift Lightspeed');
42+
render(<AskOpenShiftLightspeedButton />);
43+
expect(screen.getByRole('button')).toBeInTheDocument();
4644
});
4745

4846
it('should not render "Ask OpenShift Lightspeed" button when showLightspeedButton is false', () => {
4947
(useOLSConfig as jest.Mock).mockReturnValue(false);
50-
wrapper = shallow(<AskOpenShiftLightspeedButton />);
51-
expect(wrapper.find(Button).exists()).toBe(false);
48+
render(<AskOpenShiftLightspeedButton />);
49+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
5250
});
5351

5452
it('should dispatch OpenOLS action when "Ask OpenShift Lightspeed" button is clicked', () => {
5553
(useOLSConfig as jest.Mock).mockReturnValue(true);
56-
wrapper = shallow(<AskOpenShiftLightspeedButton />);
57-
wrapper.find(Button).simulate('click');
54+
render(<AskOpenShiftLightspeedButton />);
55+
const button = screen.getByRole('button');
56+
fireEvent.click(button);
5857
expect(mockDispatch).toHaveBeenCalledWith({ type: ActionType.OpenOLS });
5958
});
6059
});

frontend/packages/console-shared/src/components/formik-fields/CodeEditorField.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as React from 'react';
2-
import { Switch } from '@patternfly/react-core';
32
import { css } from '@patternfly/react-styles';
43
import { FormikValues, useField, useFormikContext } from 'formik';
54
import { isEmpty } from 'lodash';
@@ -15,6 +14,7 @@ import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watc
1514
import { ConsoleYAMLSampleModel } from '@console/internal/models';
1615
import { getYAMLTemplates } from '@console/internal/models/yaml-templates';
1716
import { definitionFor, K8sResourceCommon, referenceForModel } from '@console/internal/module/k8s';
17+
import { ToggleSidebarButton } from '@console/shared/src/components/editor/ToggleSidebarButton';
1818
import { getResourceSidebarSamples } from '../../utils';
1919
import { CodeEditorFieldProps } from './field-types';
2020

@@ -99,13 +99,13 @@ const CodeEditorField: React.FC<CodeEditorFieldProps> = ({
9999
language={language}
100100
toolbarLinks={[
101101
hasSidebarContent && (
102-
<Switch
103-
label={t('public~Sidebar')}
102+
<ToggleSidebarButton
103+
key="toggle-sidebar"
104104
id="showSidebar"
105-
isChecked={sidebarOpen}
106-
data-checked-state={sidebarOpen}
107-
onChange={() => setSidebarOpen(!sidebarOpen)}
108-
hasCheckIcon
105+
isSidebarOpen={sidebarOpen}
106+
toggleSidebar={() => setSidebarOpen(!sidebarOpen)}
107+
alignToEnd
108+
className="pf-v6-u-mr-xs"
109109
/>
110110
),
111111
]}

frontend/packages/console-shared/src/constants/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export const COLUMN_MANAGEMENT_LOCAL_STORAGE_KEY = `${STORAGE_PREFIX}/table-colu
4848
export const LOG_WRAP_LINES_USERSETTINGS_KEY = `${USERSETTINGS_PREFIX}.log.wrapLines`;
4949
export const SHOW_YAML_EDITOR_TOOLTIPS_USER_SETTING_KEY = `${USERSETTINGS_PREFIX}.showYAMLEditorTooltips`;
5050
export const SHOW_YAML_EDITOR_TOOLTIPS_LOCAL_STORAGE_KEY = `${STORAGE_PREFIX}/showYAMLEditorTooltips`;
51+
export const SHOW_YAML_EDITOR_STICKY_SCROLL_USER_SETTING_KEY = `${USERSETTINGS_PREFIX}.showYAMLEditorStickyScroll`;
52+
export const SHOW_YAML_EDITOR_STICKY_SCROLL_LOCAL_STORAGE_KEY = `${STORAGE_PREFIX}/showYAMLEditorStickyScroll`;
5153
export const SHOW_FULL_LOG_USERSETTINGS_KEY = `${USERSETTINGS_PREFIX}.show.full.log`;
5254
// Bootstrap user for OpenShift 4.0 clusters (kube:admin)
5355
export const KUBE_ADMIN_USERNAMES = ['kube:admin'];

frontend/packages/console-shared/src/hoc/withPostFormSubmissionCallback.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { K8sResourceCommon } from '@console/internal/module/k8s';
33
import { usePostFormSubmitAction } from '../hooks/post-form-submit-action';
44

5-
type WithPostFormSubmissionCallbackProps<R> = {
5+
export type WithPostFormSubmissionCallbackProps<R> = {
66
postFormSubmissionCallback: (arg: R) => Promise<R>;
77
};
88

0 commit comments

Comments
 (0)