Skip to content

Commit

Permalink
Add document with VNet diag report (#52289)
Browse files Browse the repository at this point in the history
* Check if TabHost story includes all possible docs

* Add DocumentVnetDiagReport type

* Ensure createdAt from diag report is serializable to JSON

* Open VNet diag report from DiagnosticsAlert

* Add document with VNet diag report

* Re-open existing diag doc if found

* Turn TODO comment into named TODO comment

* `doc?` -> `doc`

* Remove dots from alert titles

* Return early if rootClusterUri is missing
  • Loading branch information
ravicious authored Feb 20, 2025
1 parent 1ca74a4 commit 2a6a20f
Show file tree
Hide file tree
Showing 15 changed files with 858 additions and 39 deletions.
13 changes: 13 additions & 0 deletions web/packages/teleterm/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import { Kube } from 'gen-proto-ts/teleport/lib/teleterm/v1/kube_pb';
import { Server } from 'gen-proto-ts/teleport/lib/teleterm/v1/server_pb';
import { PaginatedResource } from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb';
import * as api from 'gen-proto-ts/teleport/lib/teleterm/v1/tshd_events_service_pb';
import {
CheckReport,
RouteConflictReport,
} from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';

import {
ReloginRequest,
Expand Down Expand Up @@ -171,3 +175,12 @@ export function reloginReasonOneOfIsVnetCertExpired(
} {
return reason.oneofKind === 'vnetCertExpired';
}

export function reportOneOfIsRouteConflictReport(
report: CheckReport['report']
): report is {
oneofKind: 'routeConflictReport';
routeConflictReport: RouteConflictReport;
} {
return report.oneofKind === 'routeConflictReport';
}
18 changes: 16 additions & 2 deletions web/packages/teleterm/src/services/vnet/testHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ import {
CheckReport,
CheckReportStatus,
Report,
RouteConflict,
} from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';

export const makeReport = (props: Partial<Report> = {}): Report => ({
createdAt: Timestamp.fromDate(new Date(2025, 0, 1, 12, 0)),
createdAt: Timestamp.fromDate(new Date(2024, 10, 23, 13, 27, 48)),
checks: [makeCheckAttempt()],
networkStackAttempt: {
status: CheckAttemptStatus.OK,
Expand Down Expand Up @@ -56,7 +57,20 @@ export const makeCheckReport = (
): CheckReport => ({
status: CheckReportStatus.OK,
report: {
oneofKind: undefined,
oneofKind: 'routeConflictReport',
routeConflictReport: {
routeConflicts: [],
},
},
...props,
});

export const makeRouteConflict = (
props: Partial<RouteConflict> = {}
): RouteConflict => ({
dest: '100.64.0.0/10',
vnetDest: '100.64.0.1',
interfaceName: 'utun5',
interfaceApp: '',
...props,
});
3 changes: 3 additions & 0 deletions web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
Workspace,
} from 'teleterm/ui/services/workspacesService';
import { isAppUri, isDatabaseUri, RootClusterUri } from 'teleterm/ui/uri';
import { DocumentVnetDiagReport } from 'teleterm/ui/Vnet/DocumentVnetDiagReport';

import { KeyboardShortcutsPanel } from './KeyboardShortcutsPanel';
import { WorkspaceContextProvider } from './workspaceContext';
Expand Down Expand Up @@ -171,6 +172,8 @@ function MemoizedDocument(props: { doc: types.Document; visible: boolean }) {
return <DocumentConnectMyComputer doc={doc} visible={visible} />;
case 'doc.authorize_web_session':
return <DocumentAuthorizeWebSession doc={doc} visible={visible} />;
case 'doc.vnet_diag_report':
return <DocumentVnetDiagReport doc={doc} visible={visible} />;
default:
return (
<Document visible={visible}>
Expand Down
22 changes: 21 additions & 1 deletion web/packages/teleterm/src/ui/TabHost/TabHost.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
makeDocumentGatewayKube,
makeDocumentPtySession,
makeDocumentTshNode,
makeDocumentVnetDiagReport,
} from 'teleterm/ui/services/workspacesService/documentsService/testHelpers';

import { TabHostContainer } from './TabHost';
Expand All @@ -45,7 +46,7 @@ const meta: Meta = {

export default meta;

const allDocuments: Document[] = [
const allDocuments = [
makeDocumentCluster(),
makeDocumentTshNode(),
makeDocumentConnectMyComputer(),
Expand All @@ -56,6 +57,7 @@ const allDocuments: Document[] = [
makeDocumentAccessRequests(),
makeDocumentPtySession(),
makeDocumentAuthorizeWebSession(),
makeDocumentVnetDiagReport(),
];

const cluster = makeRootCluster();
Expand All @@ -74,3 +76,21 @@ export function Story() {
</MockAppContextProvider>
);
}

// https://stackoverflow.com/questions/53807517/how-to-test-if-two-types-are-exactly-the-same/73461648#73461648
function assert<T extends never>() {} // eslint-disable-line @typescript-eslint/no-unused-vars
type TypeEqualityGuard<A, B> = Exclude<A, B> | Exclude<B, A>;
type ArrayElement<T> = T extends (infer U)[] ? U : never;

type AllExpectedDocs = Exclude<
Document,
// DocumentBlank isn't rendered with other documents in the real app.
| { kind: 'doc.blank' }
// Deprecated DocumentTshNodeWithLoginHost.
| { kind: 'doc.terminal_tsh_node'; loginHost: string }
// Deprecated DocumentTshKube.
| { kind: 'doc.terminal_tsh_kube' }
>;
// This is going to raise a type error if allDocuments does not include all expected documents
// defined in Document.
assert<TypeEqualityGuard<ArrayElement<typeof allDocuments>, AllExpectedDocs>>();
55 changes: 49 additions & 6 deletions web/packages/teleterm/src/ui/Vnet/DiagnosticsAlert.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { useEffect } from 'react';
import { act, useEffect } from 'react';

import { render, screen } from 'design/utils/testing';
import { render, screen, userEvent } from 'design/utils/testing';
import {
CheckAttemptStatus,
CheckReportStatus,
Report,
} from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';

import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient';
import { makeRootCluster } from 'teleterm/services/tshd/testHelpers';
import {
makeCheckAttempt,
makeCheckReport,
makeReport,
} from 'teleterm/services/vnet/testHelpers';
import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
import { makeDocumentConnectMyComputer } from 'teleterm/ui/services/workspacesService/documentsService/testHelpers';
import { ConnectionsContextProvider } from 'teleterm/ui/TopBar/Connections/connectionsContext';

import { DiagnosticsAlert } from './DiagnosticsAlert';
import { useVnetContext, VnetContextProvider } from './vnetContext';
Expand Down Expand Up @@ -168,10 +171,12 @@ describe('DiagnosticsAlert', () => {

render(
<MockAppContextProvider appContext={appContext}>
<VnetContextProvider>
<RunDiagnostics />
<DiagnosticsAlert />
</VnetContextProvider>
<ConnectionsContextProvider>
<VnetContextProvider>
<RunDiagnostics />
<DiagnosticsAlert />
</VnetContextProvider>
</ConnectionsContextProvider>
</MockAppContextProvider>
);

Expand All @@ -180,6 +185,44 @@ describe('DiagnosticsAlert', () => {
).toBeInTheDocument();
}
);

it('re-opens an existing document with the report', async () => {
const user = userEvent.setup();
const appContext = new MockAppContext();
const otherDoc = makeDocumentConnectMyComputer();
appContext.addRootClusterWithDoc(makeRootCluster(), otherDoc);
const report = makeReport();
appContext.vnet.runDiagnostics = () => new MockedUnaryCall({ report });

render(
<MockAppContextProvider appContext={appContext}>
<ConnectionsContextProvider>
<VnetContextProvider>
<RunDiagnostics />
<DiagnosticsAlert />
</VnetContextProvider>
</ConnectionsContextProvider>
</MockAppContextProvider>
);

// Verify that "Open Report" opens a new doc with the report.
const docsService =
appContext.workspacesService.getActiveWorkspaceDocumentService();
expect(docsService.getLocation()).toEqual(otherDoc.uri);
await user.click(await screen.findByText('Open Report'));
const reportDocUri = docsService.getLocation();
expect(reportDocUri).not.toEqual(otherDoc.uri);

// Change the active doc to some other doc.
await act(async () => {
docsService.setLocation(otherDoc.uri);
});

// Verify that clicking Open Report again opens the original doc rather than adding a new one.
await user.click(screen.getByText('Open Report'));
expect(docsService.getDocuments()).toHaveLength(2);
expect(docsService.getLocation()).toEqual(reportDocUri);
});
});

const RunDiagnostics = () => {
Expand Down
33 changes: 32 additions & 1 deletion web/packages/teleterm/src/ui/Vnet/DiagnosticsAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ import {
CheckReportStatus,
} from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';

import { useAppContext } from 'teleterm/ui/appContextProvider';
import { useStoreSelector } from 'teleterm/ui/hooks/useStoreSelector';
import { useConnectionsContext } from 'teleterm/ui/TopBar/Connections/connectionsContext';

import { textSpacing } from './sliderStep';
import { useVnetContext } from './vnetContext';

export const DiagnosticsAlert = () => {
const { diagnosticsAttempt, runDiagnostics, resetDiagnosticsAttempt } =
useVnetContext();
const { workspacesService } = useAppContext();
const { close: closeConnectionsPanel } = useConnectionsContext();
const rootClusterUri = useStoreSelector(
'workspacesService',
useCallback(state => state.rootClusterUri, [])
Expand Down Expand Up @@ -70,7 +74,34 @@ export const DiagnosticsAlert = () => {
}
: {};
const openReport = () => {
// TODO(ravicious): Open report doc.
if (!rootClusterUri) {
return;
}

const docsService =
workspacesService.getWorkspaceDocumentService(rootClusterUri);

// Check for an existing doc first. It may be present if someone re-runs diagnostics from
// within a doc, then opens the VNet panel and clicks "Open Report". The report in the panel
// and the report in the doc are equal in that case, as they both come from
// diagnosticsAttempt.data.
const existingDoc = docsService.getDocuments().find(
d =>
d.kind === 'doc.vnet_diag_report' &&
// Reports don't have IDs, so createdAt is used as a good-enough approximation of an ID.
d.report?.createdAt === report.createdAt
);
if (existingDoc) {
docsService.open(existingDoc.uri);
} else {
const doc = docsService.createVnetDiagReportDocument({
rootClusterUri,
report,
});
docsService.add(doc);
docsService.open(doc.uri);
}
closeConnectionsPanel();
};

if (
Expand Down
Loading

0 comments on commit 2a6a20f

Please sign in to comment.