Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/clickable-log-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openchoreo/backstage-plugin-openchoreo-observability': minor
---

Made component names in the project-level logging view clickable, allowing users to navigate directly to the component-scoped logging view while preserving the current time range and any relevant filters.
3 changes: 1 addition & 2 deletions packages/test-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@
"directory": "packages/test-utils"
},
"backstage": {
"role": "node-library"
"role": "common-library"
},
"scripts": {
"test": "backstage-cli package test",
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/backend-test-utils": "^1.11.3",
"@backstage/catalog-model": "^1.9.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LogEntry } from './LogEntry';
Expand Down Expand Up @@ -48,7 +49,11 @@ function renderLogEntry(
expanded,
onToggleExpand: () => setExpanded(prev => !prev),
};
return <LogEntry {...defaultProps} {...overrides} />;
return (
<MemoryRouter>
<LogEntry {...defaultProps} {...overrides} />
</MemoryRouter>
);
};
return render(<Harness />);
}
Comment thread
kavix marked this conversation as resolved.
Expand Down Expand Up @@ -97,6 +102,21 @@ describe('LogEntry', () => {
expect(screen.getByText('api-service')).toBeInTheDocument();
});

it('renders component name link with correct path', () => {
renderLogEntry({
selectedFields: [...allFields, LogEntryField.ComponentName],
entityNamespace: 'my-namespace',
entityKind: 'Component',
});

const link = screen.getByRole('link', { name: 'api-service' });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute(
'href',
'/catalog/my-namespace/component/api-service/runtime-logs',
);
});

it('expands to show metadata on row click', async () => {
const user = userEvent.setup();
renderLogEntry();
Expand Down Expand Up @@ -176,4 +196,43 @@ describe('LogEntry', () => {
expect(screen.getByText('fallback-project')).toBeInTheDocument();
expect(screen.getByText('fallback-component')).toBeInTheDocument();
});

it('navigates to component runtime logs and stops propagation on component name link click', async () => {
const user = userEvent.setup();
const onToggleExpand = jest.fn();

const LocationDisplay = () => {
const location = useLocation();
return <span data-testid="location-path">{location.pathname}</span>;
};

render(
<MemoryRouter initialEntries={['/']}>
<LogEntry
log={sampleLog}
selectedFields={[...allFields, LogEntryField.ComponentName]}
environmentName="development"
projectName="my-project"
componentName="api-service"
expanded={false}
onToggleExpand={onToggleExpand}
/>
<LocationDisplay />
</MemoryRouter>,
);

// Assert initial location is '/'
expect(screen.getByTestId('location-path')).toHaveTextContent('/');

const link = screen.getByRole('link', { name: 'api-service' });
await user.click(link);

// Assert the location updated to runtime-logs path
expect(screen.getByTestId('location-path')).toHaveTextContent(
'/catalog/default/component/api-service/runtime-logs',
);

// Assert propagation was stopped (onToggleExpand was NOT called)
expect(onToggleExpand).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import {
} from '@material-ui/core';
import FileCopyOutlined from '@material-ui/icons/FileCopyOutlined';
import { LogEntry as LogEntryType, LogEntryField } from './types';
import { Link } from '@backstage/core-components';
import { useLocation } from 'react-router-dom';
import { useLogEntryStyles } from './styles';
import { getColumnStyle } from './columns';
import { Entity } from '@backstage/catalog-model';
import { buildRuntimeLogsBasePath } from '@openchoreo/backstage-plugin-react';

/**
* Render-prop slot for a per-row action button (assistant integration,
Expand All @@ -29,6 +33,8 @@ interface LogEntryProps {
environmentName?: string;
projectName?: string;
componentName?: string;
entityNamespace?: string;
entityKind?: string;
/**
* Whether this row is currently expanded. Controlled by the parent so that
* expansion survives the virtualizer unmounting the row off-screen.
Expand All @@ -53,12 +59,15 @@ export const LogEntry: FC<LogEntryProps> = ({
environmentName,
projectName,
componentName,
entityNamespace,
entityKind,
expanded,
onToggleExpand,
getLogsSnapshot,
renderRowAction,
}) => {
const classes = useLogEntryStyles();
const location = useLocation();
const [copySuccess, setCopySuccess] = useState(false);

const handleCopyLog = async (event: MouseEvent<HTMLButtonElement>) => {
Expand Down Expand Up @@ -140,13 +149,32 @@ export const LogEntry: FC<LogEntryProps> = ({
}

if (field === LogEntryField.ComponentName) {
const compName = log.metadata?.componentName ?? componentName ?? '';
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: entityKind || 'Component',
metadata: {
name: compName,
namespace: entityNamespace,
},
};
const basePath = buildRuntimeLogsBasePath(entity);
return (
<Box
key={field}
style={getColumnStyle(field)}
className={`${classes.cell} ${classes.monospaceCell}`}
>
{log.metadata?.componentName ?? componentName ?? ''}
{compName ? (
<Link
to={`${basePath}${location.search}`}
onClick={e => e.stopPropagation()}
>
{compName}
</Link>
) : (
''
)}
</Box>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ interface LogsTableProps {
environmentName?: string;
projectName?: string;
componentName?: string;
entityNamespace?: string;
entityKind?: string;
renderRowAction?: RenderLogRowAction;
}

Expand All @@ -37,6 +39,8 @@ export const LogsTable: FC<LogsTableProps> = ({
environmentName,
projectName,
componentName,
entityNamespace,
entityKind,
renderRowAction,
}) => {
const classes = useLogsTableStyles();
Expand Down Expand Up @@ -164,6 +168,8 @@ export const LogsTable: FC<LogsTableProps> = ({
environmentName={environmentName}
projectName={projectName}
componentName={componentName}
entityNamespace={entityNamespace}
entityKind={entityKind}
expanded={expanded.has(key)}
onToggleExpand={() => toggle(key)}
getLogsSnapshot={getLogsSnapshot}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ const ObservabilityProjectRuntimeLogsContent = ({
selectedEnvironment?.displayName || selectedEnvironment?.name
}
projectName={projectName}
entityNamespace={entity.metadata.namespace}
entityKind="component"
renderRowAction={renderRowAction}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ const ObservabilityRuntimeLogsContent = ({
}
projectName={project}
componentName={componentName}
entityNamespace={entity.metadata.namespace}
entityKind={entity.kind}
renderRowAction={renderRowAction}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,10 @@ describe('SetupDetailPane', () => {
await user.click(screen.getByRole('checkbox', { name: /auto deploy/i }));
await user.click(screen.getByRole('button', { name: /confirm/i }));

await waitFor(() => {
expect(screen.queryByRole('dialog')).toBeNull();
});

await waitFor(() => {
expect(mockShowError).toHaveBeenCalledWith(
'Failed to update auto deploy setting: boom',
Expand Down Expand Up @@ -356,6 +360,10 @@ describe('SetupDetailPane', () => {
await user.click(screen.getByRole('checkbox', { name: /auto deploy/i }));
await user.click(screen.getByRole('button', { name: /confirm/i }));

await waitFor(() => {
expect(screen.queryByRole('dialog')).toBeNull();
});

expect(mockUpdateAutoDeploy).toHaveBeenCalledWith(true);
await waitFor(() => {
expect(mockShowError).toHaveBeenCalledWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ describe('CreateSecretDialog — SSH Auth', () => {
await selectSshAuth(user);
await pasteSshKey(user, VALID_KEY);
expect(screen.getByRole('button', { name: 'Create' })).toBeEnabled();
});
}, 15000);

// The two tests below interact with multiple fields (SSH Key ID + multiline
// SSH key + dynamically-added Key/Value rows). userEvent.type is per-keystroke
Expand Down
31 changes: 31 additions & 0 deletions plugins/openchoreo/src/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
/* eslint-disable no-console */
import '@testing-library/jest-dom';

(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
if (typeof window !== 'undefined') {
(window as any).IS_REACT_ACT_ENVIRONMENT = true;
}
if (typeof global !== 'undefined') {
(global as any).IS_REACT_ACT_ENVIRONMENT = true;
}

const originalConsoleError = console.error;
console.error = (...args: any[]) => {
if (
typeof args[0] === 'string' &&
(args[0].includes('Could not parse CSS stylesheet') ||
args[0].includes('css parsing') ||
args[0].includes('not configured to support act'))
) {
return;
}
if (
args[0] &&
typeof args[0] === 'object' &&
(args[0].message?.includes('Could not parse CSS stylesheet') ||
args[0].type === 'css parsing' ||
args[0].message?.includes('not configured to support act'))
) {
return;
}
originalConsoleError(...args);
};
Loading
Loading