Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
ff122a3
yaml
lucasgoral Apr 9, 2025
a8469a2
initialize
lucasgoral Apr 9, 2025
7c5dc82
improvements
lucasgoral Apr 10, 2025
57632d2
npm
lucasgoral Apr 10, 2025
f552855
fixes
lucasgoral Apr 11, 2025
d63dd7a
fixes
lucasgoral Apr 11, 2025
99ddbdb
refactor
lucasgoral Apr 11, 2025
6890ece
Merge branch 'main' into feature/display-resource-yaml
lucasgoral Apr 11, 2025
7221234
Update package-lock.json
lucasgoral Apr 11, 2025
f4226e2
Update tsconfig.json
lucasgoral Apr 14, 2025
d069cc5
graphql
lucasgoral Apr 14, 2025
5a67fb4
Update YamlViewer.tsx
lucasgoral Apr 14, 2025
49c9ce1
refactor
lucasgoral Apr 15, 2025
794967d
refactor
lucasgoral Apr 15, 2025
9e58f60
refactor
lucasgoral Apr 16, 2025
567154c
Update ControlPlaneListWorkspaceGridTile.tsx
lucasgoral Apr 16, 2025
60d9100
Update ConnectButton.tsx
lucasgoral Apr 16, 2025
bf417c3
fixes
lucasgoral Apr 17, 2025
a90d9f1
Merge branch 'main' into feature/display-resource-yaml
lucasgoral Apr 17, 2025
4d832ce
Update package-lock.json
lucasgoral Apr 17, 2025
06dbcae
Update ControlPlaneView.tsx
lucasgoral Apr 17, 2025
41354fe
fixes
lucasgoral Apr 18, 2025
d3ae2ef
fixes
lucasgoral Apr 18, 2025
f4ff4f0
fixes
lucasgoral Apr 18, 2025
092b17f
refactor
lucasgoral Apr 21, 2025
0316081
fixes
lucasgoral Apr 22, 2025
775cb8f
fixes
lucasgoral Apr 22, 2025
e598273
fixes
lucasgoral Apr 22, 2025
4a1601b
fixes
lucasgoral Apr 22, 2025
c7ab2d3
fixes
lucasgoral Apr 22, 2025
cdea1f6
remove aliases
lucasgoral Apr 23, 2025
1ed594b
refactor
lucasgoral Apr 23, 2025
ed120af
removed card prototype
lucasgoral Apr 23, 2025
0f5b052
fixes
lucasgoral Apr 23, 2025
2a142ed
fixes
lucasgoral Apr 23, 2025
dea32ec
fixes
lucasgoral Apr 23, 2025
c9f5416
Update vite.config.ts
lucasgoral Apr 23, 2025
75b845d
fixes
lucasgoral Apr 23, 2025
54174b2
revert
lucasgoral Apr 23, 2025
a07b296
Merge branch 'main' into feature/display-resource-yaml
lucasgoral Apr 23, 2025
3571ba4
Update package-lock.json
lucasgoral Apr 23, 2025
190a4b0
Update package-lock.json
lucasgoral Apr 23, 2025
4161df2
Update ProjectsList.tsx
lucasgoral Apr 24, 2025
aa13880
Update ProjectsList.tsx
lucasgoral Apr 24, 2025
096c5f4
refactor
lucasgoral Apr 24, 2025
18d3f63
fix
lucasgoral Apr 24, 2025
2bafa7a
Update YamlViewer.tsx
lucasgoral Apr 25, 2025
c2df896
Merge branch 'main' into feature/display-resource-yaml
lucasgoral Apr 25, 2025
046afb0
Update package-lock.json
lucasgoral Apr 25, 2025
428e14f
refactor
lucasgoral Apr 25, 2025
88c9baa
Update YamlViewer.tsx
lucasgoral Apr 25, 2025
b2da1b2
Merge branch 'main' into feature/display-resource-yaml
lucasgoral Apr 28, 2025
c37f57a
Update package-lock.json
lucasgoral Apr 28, 2025
77c089f
Update ProjectsList.tsx
lucasgoral Apr 28, 2025
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
1,940 changes: 894 additions & 1,046 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@
"react-i18next": "^15.4.1",
"react-oidc-context": "^3.2.0",
"react-router-dom": "^7.2.0",
"react-syntax-highlighter": "^15.6.1",
"react-time-ago": "^7.3.3",
"swr": "^2.3.0",
"yaml": "^2.7.1",
"zod": "^3.24.2"
},
"devDependencies": {
Expand All @@ -52,6 +54,7 @@
"@types/node": "^22.13.5",
"@types/react": "^19.0.10",
"@types/react-dom": "19.1.2",
"@types/react-syntax-highlighter": "^15.5.13",
"@ui5/webcomponents-cypress-commands": "^2.7.2",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/eslint-plugin": "^1.1.37",
Expand Down
10 changes: 10 additions & 0 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,5 +239,15 @@
"max25chars": "Max length is 25 characters.",
"userExists": "User with this email already exists!",
"atLeastOneUser": "You need to have at least one member assigned."
},
"common": {
"close": "Close",
"cannotLoadData": "Cannot load data"
},
"buttons": {
"viewResource": "View resource"
},
"yaml": {
"copiedToClipboard": "YAML copied to clipboard!"
}
}
1 change: 1 addition & 0 deletions src/components/ControlPlanes/ConnectButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default function ConnectButton(props: Props) {
if (contexts.length === 1) {
return (
<Button
endIcon={'navigation-right-arrow'}
disabled={props.disabled}
onClick={() =>
navigate(
Expand Down
49 changes: 31 additions & 18 deletions src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,27 @@ import '@ui5/webcomponents-fiori/dist/illustrations/NoData.js';
import '@ui5/webcomponents-fiori/dist/illustrations/EmptyList.js';
import '@ui5/webcomponents-icons/dist/delete';
import ConnectButton from '../ConnectButton.tsx';
import { ListWorkspacesType } from '../../../lib/api/types/crate/listWorkspaces.ts';

import TitleLevel from '@ui5/webcomponents/dist/types/TitleLevel.js';
import { useState } from 'react';

import { DeleteConfirmationDialog } from '../../Dialogs/DeleteConfirmationDialog.tsx';
import MCPHealthPopoverButton from '../../ControlPlane/MCPHealthPopoverButton.tsx';
import styles from './ControlPlaneCard.module.css';
import { KubectlDeleteMcp } from '../../Dialogs/KubectlCommandInfo/Controllers/KubectlDeleteMcp.tsx';
import {
ListControlPlanesType,
ReadyStatus,
} from '../../../lib/api/types/crate/controlPlanes.ts';
import TitleLevel from '@ui5/webcomponents/dist/types/TitleLevel.js';
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import { useState } from 'react';
import { ListWorkspacesType } from '../../../lib/api/types/crate/listWorkspaces.ts';
import { useApiResourceMutation } from '../../../lib/api/useApiResource.ts';
import {
DeleteMCPResource,
DeleteMCPType,
PatchMCPResourceForDeletion,
PatchMCPResourceForDeletionBody,
} from '../../../lib/api/types/crate/deleteMCP.ts';
import { DeleteConfirmationDialog } from '../../Dialogs/DeleteConfirmationDialog.tsx';
import MCPHealthPopoverButton from '../../ControlPlane/MCPHealthPopoverButton.tsx';
import styles from './ControlPlaneCard.module.css';
import { KubectlDeleteMcp } from '../../Dialogs/KubectlCommandInfo/Controllers/KubectlDeleteMcp.tsx';
import { YamlViewButton } from '../../Yaml/YamlViewButton.tsx';

interface Props {
controlPlane: ListControlPlanesType;
Expand Down Expand Up @@ -64,7 +66,6 @@ export function ControlPlaneCard({
</FlexBox>
<div>
<Button
design={ButtonDesign.Transparent}
icon="delete"
disabled={
controlPlane.status?.status === ReadyStatus.InDeletion
Expand All @@ -82,15 +83,27 @@ export function ControlPlaneCard({
className={styles.row}
>
<MCPHealthPopoverButton mcpStatus={controlPlane.status} />
<ConnectButton
disabled={controlPlane.status?.status !== ReadyStatus.Ready}
controlPlaneName={name}
projectName={projectName}
workspaceName={workspace.metadata.name ?? ''}
namespace={controlPlane.status?.access?.namespace ?? ''}
secretName={controlPlane.status?.access?.name ?? ''}
secretKey={controlPlane.status?.access?.key ?? ''}
/>
<FlexBox
direction="Row"
justifyContent="SpaceBetween"
alignItems="Center"
gap={10}
>
<YamlViewButton
workspaceName={controlPlane.metadata.namespace}
resourceName={controlPlane.metadata.name}
resourceType={'managedcontrolplanes'}
/>
<ConnectButton
disabled={controlPlane.status?.status !== ReadyStatus.Ready}
controlPlaneName={name}
projectName={projectName}
workspaceName={workspace.metadata.name ?? ''}
namespace={controlPlane.status?.access?.namespace ?? ''}
secretName={controlPlane.status?.access?.name ?? ''}
secretKey={controlPlane.status?.access?.key ?? ''}
/>
</FlexBox>
</FlexBox>
</FlexBox>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Button,
FlexBox,
Grid,
ObjectPageSection,
Panel,
Expand All @@ -8,7 +9,6 @@ import {
import '@ui5/webcomponents-fiori/dist/illustrations/NoData.js';
import '@ui5/webcomponents-fiori/dist/illustrations/EmptyList.js';
import '@ui5/webcomponents-icons/dist/delete';
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import { CopyButton } from '../../Shared/CopyButton.tsx';
import { NoManagedControlPlaneBanner } from '../NoManagedControlPlaneBanner.tsx';
import { ControlPlaneCard } from '../ControlPlaneCard/ControlPlaneCard.tsx';
Expand All @@ -34,6 +34,7 @@ import { ListControlPlanes } from '../../../lib/api/types/crate/controlPlanes.ts
import IllustratedError from '../../Shared/IllustratedError.tsx';
import { APIError } from '../../../lib/api/error.ts';
import { useTranslation } from 'react-i18next';
import { YamlViewButton } from '../../Yaml/YamlViewButton.tsx';

interface Props {
projectName: string;
Expand Down Expand Up @@ -104,8 +105,9 @@ export function ControlPlaneListWorkspaceGridTile({
style={{
width: '100%',
display: 'grid',
gridTemplateColumns: '0.3fr 0.24fr auto 0.05fr',
gridTemplateColumns: '0.3fr 0.3fr 0.24fr auto',
gap: '1rem',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
Expand All @@ -118,18 +120,25 @@ export function ControlPlaneListWorkspaceGridTile({
text={workspace.status?.namespace || '-'}
style={{ justifyContent: 'start' }}
/>

<MembersAvatarView
members={workspace.spec.members}
project={projectName}
workspace={workspaceName}
/>
<Button
design={ButtonDesign.Transparent}
icon="delete"
onClick={async () => {
setDialogDeleteWsIsOpen(true);
}}
/>
<FlexBox justifyContent={'SpaceBetween'} gap={10}>
<Button
icon="delete"
onClick={async () => {
setDialogDeleteWsIsOpen(true);
}}
/>
<YamlViewButton
workspaceName={workspace.metadata.namespace}
resourceName={workspaceName}
resourceType={'workspaces'}
/>
</FlexBox>
</div>
}
noAnimation
Expand Down
3 changes: 1 addition & 2 deletions src/components/Projects/ProjectsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CopyButton } from '../Shared/CopyButton.tsx';
import useLuigiNavigate from '../Shared/useLuigiNavigate.tsx';
import IllustratedError from '../Shared/IllustratedError.tsx';
import useResource from '../../lib/api/useApiResource';
import { projectnameToNamespace } from '../../utils/index';
import { projectnameToNamespace } from '../../utils';
import '@ui5/webcomponents-icons/dist/copy';
import '@ui5/webcomponents-icons/dist/arrow-right';
import { ListProjectNames } from '../../lib/api/types/crate/listProjectNames';
Expand All @@ -18,7 +18,6 @@ export default function ProjectsList() {
if (error) {
return <IllustratedError error={error} />;
}

return (
<>
<AnalyticalTable
Expand Down
33 changes: 33 additions & 0 deletions src/components/Yaml/YamlLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ResourceProps } from './YamlViewButton.tsx';
import { FC } from 'react';

import { stringify } from 'yaml';

import { useTranslation } from 'react-i18next';
import { ResourceObject } from '../../lib/api/types/crate/resourceObject.ts';
import Loading from '../Shared/Loading.tsx';
import IllustratedError from '../Shared/IllustratedError.tsx';
import YamlViewer from './YamlViewer.tsx';
import useResource from '../../lib/api/useApiResource';

export const YamlLoader: FC<ResourceProps> = ({
workspaceName,
resourceType,
resourceName,
}) => {
const { isLoading, data, error } = useResource(
ResourceObject(workspaceName ?? '', resourceType, resourceName),
);
const { t } = useTranslation();
if (isLoading) return <Loading />;
if (error) {
return <IllustratedError error={t('common.cannotLoadData')} />;
}

return (
<YamlViewer
yamlString={stringify(data)}
filename={`${workspaceName ? `${workspaceName}_` : ''}${resourceType}_${resourceName}`}
/>
);
};
51 changes: 51 additions & 0 deletions src/components/Yaml/YamlViewButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Bar, Button, Dialog } from '@ui5/webcomponents-react';
import { FC, useState } from 'react';
import { YamlLoader } from './YamlLoader.tsx';
import { useTranslation } from 'react-i18next';

export type ResourceProps = {
workspaceName?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this ever be undefined?

Just wondering if we can simply write

Suggested change
workspaceName?: string;
workspaceName: string;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for the Project YAML it is empty.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, in this case I’m wondering what if we made this a bit more generic by decoupling from the workspace/project context? Something like:

export type YamlViewButtonProps = {
  apiPath: string;
  localFilename: string;
}

After all, it’s essentially just a dialog that displays downloads and YAML content. It probably shouldn’t need to know the specifics of how to retrieve that data.

Just thinking out loud here - we’ll likely need to revisit the fetch logic when we implement GraphQL anyway, so we can see how this approach works then.

resourceType: 'projects' | 'workspaces' | 'managedcontrolplanes';
resourceName: string;
};

export const YamlViewButton: FC<ResourceProps> = ({
workspaceName,
resourceType,
resourceName,
}) => {
const [isOpen, setIsOpen] = useState(false);
const { t } = useTranslation();
return (
<span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for the <span>?

Suggested change
<span>
<>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is wrapped in span so it it display: inline instead of display:block. So I can put it next to some text.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn’t <> instead of <span> put the UI5 button (<Button>) directly to the top level which has display:inline-block? Anyway, not a big deal, was just wondering.

<Dialog
open={isOpen}
stretch
footer={
<Bar
design="Footer"
endContent={
<Button design="Emphasized" onClick={() => setIsOpen(false)}>
{t('common.close')}
</Button>
}
/>
}
>
<YamlLoader
workspaceName={workspaceName}
resourceName={resourceName}
resourceType={resourceType}
/>
</Dialog>
<Button
icon="document"
aria-label={t('buttons.viewResource')}
title={t('buttons.viewResource')}
onClick={() => {
setIsOpen(true);
}}
/>
</span>
);
};
10 changes: 10 additions & 0 deletions src/components/Yaml/YamlViewer.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.container {
position: relative;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m wondering if we should change the overflow to get rid of the horizontal scrollbar, although it feels a bit hacky. What do you think?

Suggested change
position: relative;
position: relative;
overflow-x: clip;

I don’t think there’s any guarantee that we can fit the content horizontally to the screen, so maybe not the best idea. 🤔

}

.buttons {
position: sticky;
top: 0;
right: 0;
z-index: 1;
}
85 changes: 85 additions & 0 deletions src/components/Yaml/YamlViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { FC } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import {
materialLight,
materialDark,
} from 'react-syntax-highlighter/dist/esm/styles/prism'; // You can choose different styles
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} from 'react-syntax-highlighter/dist/esm/styles/prism'; // You can choose different styles
} from 'react-syntax-highlighter/dist/esm/styles/prism';

Good to know, though 😃

import YAML from 'yaml';
import { Button, FlexBox } from '@ui5/webcomponents-react';
import styles from './YamlViewer.module.css';
import { useToast } from '../../context/ToastContext.tsx';
import { useTranslation } from 'react-i18next';
type YamlViewerProps = { yamlString: string; filename: string };
const YamlViewer: FC<YamlViewerProps> = ({ yamlString, filename }) => {
const toast = useToast();
const { t } = useTranslation();
const copyToClipboard = () => {
navigator.clipboard.writeText(yamlString);
toast.show(t('yaml.copiedToClipboard'));
};
Comment on lines +18 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could create a custom hook that handles both error handling and potentially a countdown timer (similar to other copy buttons that update their display after a few seconds).

For example, something like: ⁠const { copy, recentlyCopied } = useCopyToClipboard(2000);

What are your thoughts on this? If you agree, I’d suggest adding it as a new backlog item (i.e. not as a part of this pull request).

const downloadYaml = () => {
const blob = new Blob([yamlString], { type: 'text/yaml' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${filename}.yaml`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
};
Comment on lines +22 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed we already have a ⁠DownloadKubeconfig function that handles file downloads.

I think it might be worth considering extracting this logic into a separate function, like ⁠useFileDownload().

Perhaps we could tackle this refactoring in a follow-up task? What are your thoughts?


let formattedYaml = yamlString;
try {
const parsed = YAML.parse(yamlString);
formattedYaml = YAML.stringify(parsed);
} catch (error) {
console.error('Invalid YAML:', error);
}
return (
<div className={styles.container}>
<FlexBox
className={styles.buttons}
direction="Row"
justifyContent="End"
alignItems="Baseline"
gap={16}
>
<Button icon="copy" onClick={copyToClipboard}>
Copy
</Button>
<Button icon="download" onClick={downloadYaml}>
Download
</Button>
</FlexBox>
<SyntaxHighlighter
language="yaml"
style={
window.matchMedia('(prefers-color-scheme: dark)').matches
? materialDark
: materialLight
}
showLineNumbers
wrapLines
wrapLongLines
lineNumberStyle={{
paddingRight: '20px',
minWidth: '40px',
textAlign: 'right',
}}
customStyle={{
margin: 0,
padding: '20px',
borderRadius: '4px',
fontSize: '1rem',
width: '100%',
background: 'transparent',
}}
>
{formattedYaml}
</SyntaxHighlighter>
</div>
);
};

export default YamlViewer;
Loading
Loading