diff --git a/packages/manager/.changeset/pr-13492-added-1773404986416.md b/packages/manager/.changeset/pr-13492-added-1773404986416.md new file mode 100644 index 00000000000..cbd40ecf891 --- /dev/null +++ b/packages/manager/.changeset/pr-13492-added-1773404986416.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Stream Metrics tab with embedded metrics dashboard ([#13492](https://github.com/linode/manager/pull/13492)) diff --git a/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts index 2b868c07260..3c9da9e446c 100644 --- a/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts @@ -92,7 +92,7 @@ function editDestinationViaActionMenu( mockGetDestination(destination); // Edit destination redirect ui.actionMenuItem.findByTitle('Edit').click(); - cy.url().should('endWith', `/destinations/${destination.id}/edit`); + cy.url().should('endWith', `/destinations/${destination.id}/summary`); }); } @@ -181,7 +181,7 @@ describe('destinations landing checks for non-empty state', () => { }); }); - it('navigates to edit page when clicking destination label', () => { + it('navigates to summary page when clicking destination label', () => { cy.visitWithLogin('/logs/delivery/destinations'); cy.wait('@getDestinations'); @@ -189,7 +189,7 @@ describe('destinations landing checks for non-empty state', () => { mockGetDestination(destination).as('getDestination'); cy.findByText(destination.label).click(); - cy.url().should('endWith', `/destinations/${destination.id}/edit`); + cy.url().should('endWith', `/destinations/${destination.id}/summary`); cy.wait('@getDestination'); }); @@ -255,7 +255,7 @@ describe('destinations landing checks for non-empty state', () => { mockGetDestination(destination).as('getDestination'); cy.findByText(destination.label).click(); - cy.url().should('endWith', `/destinations/${destination.id}/edit`); + cy.url().should('endWith', `/destinations/${destination.id}/summary`); cy.wait('@getDestination'); }); diff --git a/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts b/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts index 9ff583e6185..215faddceef 100644 --- a/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts @@ -43,7 +43,7 @@ describe('Edit Destination', () => { describe('given Akamai Object Storage type destination', () => { beforeEach(() => { cy.visitWithLogin( - `/logs/delivery/destinations/${mockAkamaiObjectStorageDestination.id}/edit` + `/logs/delivery/destinations/${mockAkamaiObjectStorageDestination.id}/summary` ); mockGetDestination(mockAkamaiObjectStorageDestination); }); @@ -199,7 +199,7 @@ describe('Edit Destination', () => { describe('given Custom HTTPS type destination', () => { beforeEach(() => { cy.visitWithLogin( - `/logs/delivery/destinations/${mockCustomHttpsDestination.id}/edit` + `/logs/delivery/destinations/${mockCustomHttpsDestination.id}/summary` ); mockGetDestination(mockCustomHttpsDestination); }); diff --git a/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts b/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts index cfc64627750..eb8bf3d2ddc 100644 --- a/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts @@ -45,9 +45,7 @@ describe('Edit Stream', () => { mockGetStream(mockAuditLogsStream); // Visit the Edit Stream page - cy.visitWithLogin( - `/logs/delivery/streams/${mockAuditLogsStream.id}/edit/` - ); + cy.visitWithLogin(`/logs/delivery/streams/${mockAuditLogsStream.id}`); const updatedLabel = randomLabel(); @@ -206,9 +204,7 @@ describe('Edit Stream', () => { mockGetClusters([cluster1, cluster2, cluster3, cluster4]); // Visit the Edit Stream page - cy.visitWithLogin( - `/logs/delivery/streams/${mockLKEAuditLogsStream.id}/edit/` - ); + cy.visitWithLogin(`/logs/delivery/streams/${mockLKEAuditLogsStream.id}`); const updatedLabel = randomLabel(); diff --git a/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts index 2f4087cfadf..f4382119e3c 100644 --- a/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts @@ -88,7 +88,7 @@ function editStreamViaActionMenu(tableAlias: string, stream: Stream) { mockGetStream(stream); // Edit stream redirect ui.actionMenuItem.findByTitle('Edit').click(); - cy.url().should('endWith', `/streams/${stream.id}/edit`); + cy.url().should('endWith', `/streams/${stream.id}/summary`); }); } @@ -180,7 +180,7 @@ describe('Streams non-empty landing page', () => { // Redirect to stream edit page via name cy.findByText(exampleStream.label).click(); - cy.url().should('endWith', `/streams/${exampleStream.id}/edit`); + cy.url().should('endWith', `/streams/${exampleStream.id}/summary`); cy.wait(['@getStream', '@getDestinations']); // Redirect to stream edit page via menu item diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 3e59d492994..bc8233e812b 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -134,6 +134,10 @@ interface AclpLogsFlag extends BetaFeatureFlag { * This property indicates whether to show Custom HTTPS destination type */ customHttpsEnabled?: boolean; + /** + * This property indicates whether to show the "Metrics" tab on Logs Stream details page or not + */ + metricsEnabled?: boolean; /** * This property indicates whether the feature is new or not */ diff --git a/packages/manager/src/features/Delivery/Destinations/DestinationForm/DestinationEdit.test.tsx b/packages/manager/src/features/Delivery/Destinations/DestinationForm/DestinationEdit.test.tsx index 4e24b0946cd..3ee4599a276 100644 --- a/packages/manager/src/features/Delivery/Destinations/DestinationForm/DestinationEdit.test.tsx +++ b/packages/manager/src/features/Delivery/Destinations/DestinationForm/DestinationEdit.test.tsx @@ -214,14 +214,14 @@ describe('DestinationEdit', () => { }); }); - describe('given Test Connection and Edit Destination buttons', () => { + describe('given Test Connection and Save Changes buttons', () => { const testConnectionButtonText = 'Test Connection'; const saveDestinationButtonText = 'Save Changes'; const editDestinationSpy = vi.fn(); const verifyDestinationSpy = vi.fn(); describe('when Test Connection button clicked and connection verified positively', () => { - it("should enable Edit Destination button and perform proper call when it's clicked", async () => { + it("should enable Save Changes button and perform proper call when it's clicked", async () => { server.use( http.get(`*/monitor/streams/destinations/${destinationId}`, () => { return HttpResponse.json(mockDestination); @@ -267,7 +267,7 @@ describe('DestinationEdit', () => { }); describe('when Test Connection button clicked and connection verified negatively', () => { - it('should not enable Edit Destination button', async () => { + it('should not enable Save Changes button', async () => { server.use( http.get(`*/monitor/streams/destinations/${destinationId}`, () => { return HttpResponse.json(mockDestination); diff --git a/packages/manager/src/features/Delivery/Destinations/DestinationForm/DestinationEdit.tsx b/packages/manager/src/features/Delivery/Destinations/DestinationForm/DestinationEdit.tsx index f29decb6f2e..ff299dea6c9 100644 --- a/packages/manager/src/features/Delivery/Destinations/DestinationForm/DestinationEdit.tsx +++ b/packages/manager/src/features/Delivery/Destinations/DestinationForm/DestinationEdit.tsx @@ -25,7 +25,7 @@ import type { DestinationFormType } from 'src/features/Delivery/Shared/types'; export const DestinationEdit = () => { const navigate = useNavigate(); const { destinationId } = useParams({ - from: '/logs/delivery/destinations/$destinationId/edit', + from: '/logs/delivery/destinations/$destinationId/summary', }); const { mutateAsync: updateDestination, isPending: isUpdatingDestination } = useUpdateDestinationMutation(); @@ -37,7 +37,7 @@ export const DestinationEdit = () => { const landingHeaderProps: LandingHeaderProps = { breadcrumbProps: { - pathname: '/logs/delivery/destinations/edit', + pathname: '/logs/delivery/destinations/summary', crumbOverrides: [ { label: 'Delivery', @@ -48,7 +48,7 @@ export const DestinationEdit = () => { }, docsLink: 'https://techdocs.akamai.com/cloud-computing/docs/log-delivery', removeCrumbX: [1, 2], - title: `Edit Destination ${destinationId}`, + title: `Destination ${destinationId}`, }; const form = useForm({ @@ -114,7 +114,7 @@ export const DestinationEdit = () => { return ( <> - + {isLoading && ( diff --git a/packages/manager/src/features/Delivery/Destinations/DestinationForm/destinationEditLazyRoute.ts b/packages/manager/src/features/Delivery/Destinations/DestinationForm/destinationEditLazyRoute.ts index 093277b3020..c127cc48a6a 100644 --- a/packages/manager/src/features/Delivery/Destinations/DestinationForm/destinationEditLazyRoute.ts +++ b/packages/manager/src/features/Delivery/Destinations/DestinationForm/destinationEditLazyRoute.ts @@ -3,7 +3,7 @@ import { createLazyRoute } from '@tanstack/react-router'; import { DestinationEdit } from 'src/features/Delivery/Destinations/DestinationForm/DestinationEdit'; export const destinationEditLazyRoute = createLazyRoute( - '/logs/delivery/destinations/$destinationId/edit' + '/logs/delivery/destinations/$destinationId/summary' )({ component: DestinationEdit, }); diff --git a/packages/manager/src/features/Delivery/Destinations/DestinationTableRow.tsx b/packages/manager/src/features/Delivery/Destinations/DestinationTableRow.tsx index ec5e03ef2b0..a0fa0341b6e 100644 --- a/packages/manager/src/features/Delivery/Destinations/DestinationTableRow.tsx +++ b/packages/manager/src/features/Delivery/Destinations/DestinationTableRow.tsx @@ -25,7 +25,7 @@ export const DestinationTableRow = React.memo( {destination.label} diff --git a/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.test.tsx b/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.test.tsx index 3921315e7f0..7405ba9d2c5 100644 --- a/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.test.tsx +++ b/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.test.tsx @@ -166,7 +166,7 @@ describe('Destinations Landing Table', () => { await clickOnActionMenuItem('Edit'); expect(mockNavigate).toHaveBeenCalledWith({ - to: '/logs/delivery/destinations/1/edit', + to: '/logs/delivery/destinations/1/summary', }); }); }); diff --git a/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.tsx b/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.tsx index 4576a0a2207..1aec785beb2 100644 --- a/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.tsx +++ b/packages/manager/src/features/Delivery/Destinations/DestinationsLanding.tsx @@ -105,7 +105,7 @@ export const DestinationsLanding = () => { } const handleEdit = ({ id }: Destination) => { - navigate({ to: `/logs/delivery/destinations/${id}/edit` }); + navigate({ to: `/logs/delivery/destinations/${id}/summary` }); }; const openDeleteDialog = (destination: Destination) => { diff --git a/packages/manager/src/features/Delivery/Streams/Stream/StreamLanding.test.tsx b/packages/manager/src/features/Delivery/Streams/Stream/StreamLanding.test.tsx new file mode 100644 index 00000000000..44f948fd2ad --- /dev/null +++ b/packages/manager/src/features/Delivery/Streams/Stream/StreamLanding.test.tsx @@ -0,0 +1,123 @@ +import { screen } from '@testing-library/react'; +import * as React from 'react'; +import { beforeEach, describe, it } from 'vitest'; + +import { + akamaiObjectStorageDestinationFactory, + streamFactory, +} from 'src/factories'; +import { StreamLanding } from 'src/features/Delivery/Streams/Stream/StreamLanding'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import type { Flags } from 'src/featureFlags'; + +const queryMocks = vi.hoisted(() => ({ + useStreamQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + useStreamQuery: queryMocks.useStreamQuery, + }; +}); + +const streamId = 123; +const mockDestinations = [ + akamaiObjectStorageDestinationFactory.build({ id: 1 }), +]; +const mockStream = streamFactory.build({ + id: streamId, + label: `Stream ${streamId}`, + destinations: mockDestinations, +}); + +describe('StreamLanding', () => { + const renderComponent = (flags: Partial) => { + renderWithTheme(, { + flags, + initialRoute: '/logs/delivery/streams/$streamId/summary', + }); + }; + + describe('and stream has loaded successfully', () => { + beforeEach(async () => { + queryMocks.useStreamQuery.mockReturnValue({ + data: mockStream, + isLoading: false, + }); + }); + + describe('and metrics are not enabled', () => { + const flags = { + aclpLogs: { + enabled: true, + beta: false, + metricsEnabled: false, + }, + }; + + it('should render the summary and not the metrics tab', async () => { + renderComponent(flags); + + screen.getByText('Summary'); + expect(screen.queryByText('Metrics')).not.toBeInTheDocument(); + }); + }); + + describe('and metrics are enabled', () => { + const flags = { + aclpLogs: { + enabled: true, + beta: false, + metricsEnabled: true, + }, + }; + + it('should render the summary tab and metrics tab', async () => { + renderComponent(flags); + + screen.getByText('Summary'); + expect(screen.queryByText('Metrics')).toBeInTheDocument(); + }); + }); + }); + + describe('and stream is loading', () => { + beforeEach(async () => { + queryMocks.useStreamQuery.mockReturnValue({ + isLoading: true, + }); + }); + + it('should render loading spinner', async () => { + renderComponent({}); + + expect(screen.queryByText('Summary')).not.toBeInTheDocument(); + expect(screen.queryByText('Metrics')).not.toBeInTheDocument(); + + const loadingElement = screen.queryByTestId('circle-progress'); + expect(loadingElement).toBeInTheDocument(); + }); + }); + + describe('and stream request threw error', () => { + const streamErrorMessage = 'Stream not found'; + beforeEach(async () => { + queryMocks.useStreamQuery.mockReturnValue({ + isLoading: false, + error: [{ reason: streamErrorMessage }], + }); + }); + + it('should render error state with message', async () => { + renderComponent({}); + + expect(screen.queryByText('Summary')).not.toBeInTheDocument(); + expect(screen.queryByText('Metrics')).not.toBeInTheDocument(); + + expect(screen.queryByText(streamErrorMessage)).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/src/features/Delivery/Streams/Stream/StreamLanding.tsx b/packages/manager/src/features/Delivery/Streams/Stream/StreamLanding.tsx new file mode 100644 index 00000000000..788deb9b873 --- /dev/null +++ b/packages/manager/src/features/Delivery/Streams/Stream/StreamLanding.tsx @@ -0,0 +1,110 @@ +import { useStreamQuery } from '@linode/queries'; +import { Box, CircleProgress, ErrorState } from '@linode/ui'; +import { useParams } from '@tanstack/react-router'; +import * as React from 'react'; +import { useMemo } from 'react'; + +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { + LandingHeader, + type LandingHeaderProps, +} from 'src/components/LandingHeader'; +import { SuspenseLoader } from 'src/components/SuspenseLoader'; +import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; +import { TabPanels } from 'src/components/Tabs/TabPanels'; +import { Tabs } from 'src/components/Tabs/Tabs'; +import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList'; +import { useIsACLPLogsEnabled } from 'src/features/Delivery/deliveryUtils'; +import { StreamMetrics } from 'src/features/Delivery/Streams/Stream/StreamMetrics'; +import { StreamEdit } from 'src/features/Delivery/Streams/StreamForm/StreamEdit'; +import { useTabs } from 'src/hooks/useTabs'; +import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; + +import type { Tab } from 'src/hooks/useTabs'; + +export const StreamLanding = () => { + const { streamId } = useParams({ + strict: false, + }); + const { isACLPLogsMetricsEnabled } = useIsACLPLogsEnabled(); + + const activeTabs = useMemo(() => { + const result: Tab[] = [ + { + title: 'Summary', + to: `/logs/delivery/streams/$streamId/summary`, + }, + ]; + + if (isACLPLogsMetricsEnabled) { + result.push({ + title: 'Metrics', + to: `/logs/delivery/streams/$streamId/metrics`, + }); + } + + return result; + }, [isACLPLogsMetricsEnabled]); + + const { handleTabChange, tabIndex, tabs } = useTabs(activeTabs); + + const { isLoading: isLoadingStream, error: errorStream } = useStreamQuery( + Number(streamId) + ); + const streamErrorDefaultMessage = + 'There was an error retrieving stream. Please reload and try again.'; + + const landingHeaderProps: LandingHeaderProps = { + breadcrumbProps: { + pathname: `/logs/delivery/streams/${streamId}`, + crumbOverrides: [ + { + label: 'Delivery', + linkTo: '/logs/delivery/streams', + position: 1, + }, + ], + }, + docsLink: 'https://techdocs.akamai.com/cloud-computing/docs/log-delivery', + removeCrumbX: [1, 2], + title: `Stream ${streamId}`, + }; + + if (isLoadingStream) { + return ( + + + + ); + } + + if (errorStream) { + return ( + + ); + } + + return ( + <> + + + + + }> + + + + + + + + + + + + ); +}; diff --git a/packages/manager/src/features/Delivery/Streams/Stream/StreamMetrics.tsx b/packages/manager/src/features/Delivery/Streams/Stream/StreamMetrics.tsx new file mode 100644 index 00000000000..8dcec061d64 --- /dev/null +++ b/packages/manager/src/features/Delivery/Streams/Stream/StreamMetrics.tsx @@ -0,0 +1,14 @@ +import { useParams } from '@tanstack/react-router'; +import * as React from 'react'; + +import { CloudPulseDashboardWithFilters } from 'src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters'; + +export const StreamMetrics = () => { + const { streamId } = useParams({ + from: '/logs/delivery/streams/$streamId/metrics', + }); + + return ( + + ); +}; diff --git a/packages/manager/src/features/Delivery/Streams/Stream/streamLandingLazyRoute.ts b/packages/manager/src/features/Delivery/Streams/Stream/streamLandingLazyRoute.ts new file mode 100644 index 00000000000..df6a611184f --- /dev/null +++ b/packages/manager/src/features/Delivery/Streams/Stream/streamLandingLazyRoute.ts @@ -0,0 +1,9 @@ +import { createLazyRoute } from '@tanstack/react-router'; + +import { StreamLanding } from 'src/features/Delivery/Streams/Stream/StreamLanding'; + +export const streamLandingLazyRoute = createLazyRoute( + '/logs/delivery/streams/$streamId' +)({ + component: StreamLanding, +}); diff --git a/packages/manager/src/features/Delivery/Streams/StreamForm/StreamEdit.test.tsx b/packages/manager/src/features/Delivery/Streams/StreamForm/StreamEdit.test.tsx index bb2f144b5cc..d2280ef7e63 100644 --- a/packages/manager/src/features/Delivery/Streams/StreamForm/StreamEdit.test.tsx +++ b/packages/manager/src/features/Delivery/Streams/StreamForm/StreamEdit.test.tsx @@ -86,7 +86,7 @@ describe('StreamEdit', () => { }); describe( - 'given Test Connection and Edit Stream buttons', + 'given Test Connection and Save Changes buttons', { timeout: 10000 }, () => { const testConnectionButtonText = 'Test Connection'; @@ -128,7 +128,7 @@ describe('StreamEdit', () => { const createDestinationSpy = vi.fn(); const verifyDestinationSpy = vi.fn(); - it("should enable Edit Stream button and perform proper calls when it's clicked", async () => { + it("should enable Save Changes button and perform proper calls when it's clicked", async () => { server.use( http.get('*/monitor/streams/destinations', () => { return HttpResponse.json(makeResourcePage(mockDestinations)); @@ -188,7 +188,7 @@ describe('StreamEdit', () => { const editStreamSpy = vi.fn(); const createDestinationSpy = vi.fn(); - it("should enable Edit Stream button and perform proper calls when it's clicked", async () => { + it("should enable Save Changes button and perform proper calls when it's clicked", async () => { server.use( http.get('*/monitor/streams/destinations', () => { return HttpResponse.json(makeResourcePage(mockDestinations)); @@ -223,7 +223,7 @@ describe('StreamEdit', () => { name: saveStreamButtonText, }); - // Edit stream button should not be disabled with existing destination selected + // Save Changes button should not be disabled with existing destination selected expect(editStreamButton).toBeEnabled(); // Test connection should be disabled when using existing destination @@ -240,7 +240,7 @@ describe('StreamEdit', () => { }); describe('and stream has status: provisioning', () => { - it('should have disabled Edit Stream button and show info tooltip', async () => { + it('should have disabled Save Changes button and show info tooltip', async () => { server.use( http.get('*/monitor/streams/destinations', () => { return HttpResponse.json(makeResourcePage(mockDestinations)); @@ -263,7 +263,7 @@ describe('StreamEdit', () => { name: saveStreamButtonText, }); - // Edit stream button should be disabled + // Save Changes button should be disabled expect(editStreamButton).toBeDisabled(); // Edit stream @@ -286,7 +286,7 @@ describe('StreamEdit', () => { describe('when form properly filled out and Test Connection button clicked and connection verified negatively', () => { const verifyDestinationSpy = vi.fn(); - it('should not enable Edit Stream button', async () => { + it('should not enable Save Changes button', async () => { server.use( http.get('*/monitor/streams/destinations', () => { return HttpResponse.json(makeResourcePage(mockDestinations)); diff --git a/packages/manager/src/features/Delivery/Streams/StreamForm/StreamEdit.tsx b/packages/manager/src/features/Delivery/Streams/StreamForm/StreamEdit.tsx index ccdb5bf11ce..c71a00b1ae7 100644 --- a/packages/manager/src/features/Delivery/Streams/StreamForm/StreamEdit.tsx +++ b/packages/manager/src/features/Delivery/Streams/StreamForm/StreamEdit.tsx @@ -8,45 +8,22 @@ import * as React from 'react'; import { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import { - LandingHeader, - type LandingHeaderProps, -} from 'src/components/LandingHeader'; import { StreamForm } from 'src/features/Delivery/Streams/StreamForm/StreamForm'; import type { StreamAndDestinationFormType } from 'src/features/Delivery/Streams/StreamForm/types'; export const StreamEdit = () => { const { streamId } = useParams({ - from: '/logs/delivery/streams/$streamId/edit', + from: '/logs/delivery/streams/$streamId/summary', }); const { data: destinations, isLoading: isLoadingDestinations, error: errorDestinations, } = useAllDestinationsQuery(); - const { - data: stream, - isLoading: isLoadingStream, - error: errorStream, - } = useStreamQuery(Number(streamId)); - - const landingHeaderProps: LandingHeaderProps = { - breadcrumbProps: { - pathname: '/logs/delivery/streams/edit', - crumbOverrides: [ - { - label: 'Delivery', - linkTo: '/logs/delivery/streams', - position: 1, - }, - ], - }, - docsLink: 'https://techdocs.akamai.com/cloud-computing/docs/log-delivery', - removeCrumbX: [1, 2], - title: `Edit Stream ${streamId}`, - }; + const { data: stream, isLoading: isLoadingStream } = useStreamQuery( + Number(streamId) + ); const form = useForm({ defaultValues: { @@ -106,33 +83,22 @@ export const StreamEdit = () => { return ( <> - - {(isLoadingStream || isLoadingDestinations) && ( )} - {errorStream && ( - - )} {errorDestinations && ( )} - {!isLoadingStream && - !isLoadingDestinations && - !errorStream && - !errorDestinations && ( - - - - )} + {!isLoadingStream && !isLoadingDestinations && !errorDestinations && ( + + + + )} ); }; diff --git a/packages/manager/src/features/Delivery/Streams/StreamForm/streamEditLazyRoute.ts b/packages/manager/src/features/Delivery/Streams/StreamForm/streamEditLazyRoute.ts deleted file mode 100644 index 818107451b4..00000000000 --- a/packages/manager/src/features/Delivery/Streams/StreamForm/streamEditLazyRoute.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createLazyRoute } from '@tanstack/react-router'; - -import { StreamEdit } from 'src/features/Delivery/Streams/StreamForm/StreamEdit'; - -export const streamEditLazyRoute = createLazyRoute( - '/logs/delivery/streams/$streamId/edit' -)({ - component: StreamEdit, -}); diff --git a/packages/manager/src/features/Delivery/Streams/StreamTableRow.tsx b/packages/manager/src/features/Delivery/Streams/StreamTableRow.tsx index 44b92c1fddc..2974917b468 100644 --- a/packages/manager/src/features/Delivery/Streams/StreamTableRow.tsx +++ b/packages/manager/src/features/Delivery/Streams/StreamTableRow.tsx @@ -32,7 +32,7 @@ export const StreamTableRow = React.memo((props: StreamTableRowProps) => { {stream.label} diff --git a/packages/manager/src/features/Delivery/Streams/StreamsLanding.test.tsx b/packages/manager/src/features/Delivery/Streams/StreamsLanding.test.tsx index 30f2a794027..b0151d047f5 100644 --- a/packages/manager/src/features/Delivery/Streams/StreamsLanding.test.tsx +++ b/packages/manager/src/features/Delivery/Streams/StreamsLanding.test.tsx @@ -172,7 +172,7 @@ describe('Streams Landing Table', () => { await clickOnActionMenuItem('Edit'); expect(mockNavigate).toHaveBeenCalledWith({ - to: '/logs/delivery/streams/1/edit', + to: '/logs/delivery/streams/1/summary', }); }); }); diff --git a/packages/manager/src/features/Delivery/Streams/StreamsLanding.tsx b/packages/manager/src/features/Delivery/Streams/StreamsLanding.tsx index 308e96def73..1c5ea913159 100644 --- a/packages/manager/src/features/Delivery/Streams/StreamsLanding.tsx +++ b/packages/manager/src/features/Delivery/Streams/StreamsLanding.tsx @@ -125,7 +125,7 @@ export const StreamsLanding = () => { } const handleEdit = ({ id }: Stream) => { - navigate({ to: `/logs/delivery/streams/${id}/edit` }); + navigate({ to: `/logs/delivery/streams/${id}/summary` }); }; const openDeleteDialog = (stream: Stream) => { diff --git a/packages/manager/src/features/Delivery/deliveryUtils.ts b/packages/manager/src/features/Delivery/deliveryUtils.ts index 91c0be442dd..f08f8106708 100644 --- a/packages/manager/src/features/Delivery/deliveryUtils.ts +++ b/packages/manager/src/features/Delivery/deliveryUtils.ts @@ -39,6 +39,7 @@ export const useIsACLPLogsEnabled = (): { isACLPLogsBeta: boolean; isACLPLogsCustomHttpsEnabled: boolean; isACLPLogsEnabled: boolean; + isACLPLogsMetricsEnabled: boolean; isACLPLogsNew: boolean; } => { const { data: account } = useAccount(); @@ -55,6 +56,7 @@ export const useIsACLPLogsEnabled = (): { return { isACLPLogsBeta: !!flags.aclpLogs?.beta, isACLPLogsCustomHttpsEnabled: !!flags.aclpLogs?.customHttpsEnabled, + isACLPLogsMetricsEnabled: !!flags.aclpLogs?.metricsEnabled, isACLPLogsNew: !!flags.aclpLogs?.new, isACLPLogsEnabled, }; diff --git a/packages/manager/src/routes/delivery/index.ts b/packages/manager/src/routes/delivery/index.ts index a0fed0209b6..b4a66c208a5 100644 --- a/packages/manager/src/routes/delivery/index.ts +++ b/packages/manager/src/routes/delivery/index.ts @@ -22,16 +22,18 @@ const deliveryLandingRoute = createRoute({ }, getParentRoute: () => deliveryRoute, path: '/', -}).lazy(() => - import('src/features/Delivery/deliveryLandingLazyRoute').then( - (m) => m.deliveryLandingLazyRoute - ) -); +}); const streamsRoute = createRoute({ getParentRoute: () => deliveryRoute, path: 'streams', validateSearch: (search: StreamSearchParams) => search, +}); + +const streamsLandingRoute = createRoute({ + getParentRoute: () => streamsRoute, + path: '/', + validateSearch: (search: StreamSearchParams) => search, }).lazy(() => import('src/features/Delivery/deliveryLandingLazyRoute').then( (m) => m.deliveryLandingLazyRoute @@ -39,16 +41,16 @@ const streamsRoute = createRoute({ ); const streamsCreateRoute = createRoute({ - getParentRoute: () => deliveryRoute, - path: 'streams/create', + getParentRoute: () => streamsRoute, + path: 'create', }).lazy(() => import('src/features/Delivery/Streams/StreamForm/streamCreateLazyRoute').then( (m) => m.streamCreateLazyRoute ) ); -const streamsEditRoute = createRoute({ - getParentRoute: () => deliveryRoute, +const streamRoute = createRoute({ + getParentRoute: () => streamsRoute, params: { parse: ({ streamId }: { streamId: string }) => ({ streamId: Number(streamId), @@ -57,10 +59,45 @@ const streamsEditRoute = createRoute({ streamId: String(streamId), }), }, - path: 'streams/$streamId/edit', + path: '$streamId', +}); + +const streamLandingRoute = createRoute({ + beforeLoad: ({ params }) => { + throw redirect({ + to: '/logs/delivery/streams/$streamId/summary', + params: { streamId: params.streamId }, + replace: true, + }); + }, + getParentRoute: () => streamRoute, + path: '/', +}); + +const streamSummaryRoute = createRoute({ + getParentRoute: () => streamRoute, + path: 'summary', +}).lazy(() => + import('src/features/Delivery/Streams/Stream/streamLandingLazyRoute').then( + (m) => m.streamLandingLazyRoute + ) +); + +const streamMetricsRoute = createRoute({ + beforeLoad: ({ params, context }) => { + if (!context?.flags?.aclpLogs?.metricsEnabled) { + throw redirect({ + to: '/logs/delivery/streams/$streamId/summary', + params: { streamId: params.streamId }, + replace: true, + }); + } + }, + getParentRoute: () => streamRoute, + path: 'metrics', }).lazy(() => - import('src/features/Delivery/Streams/StreamForm/streamEditLazyRoute').then( - (m) => m.streamEditLazyRoute + import('src/features/Delivery/Streams/Stream/streamLandingLazyRoute').then( + (m) => m.streamLandingLazyRoute ) ); @@ -72,6 +109,12 @@ const destinationsRoute = createRoute({ getParentRoute: () => deliveryRoute, path: 'destinations', validateSearch: (search: DestinationSearchParams) => search, +}); + +const destinationsLandingRoute = createRoute({ + getParentRoute: () => destinationsRoute, + path: '/', + validateSearch: (search: DestinationSearchParams) => search, }).lazy(() => import('src/features/Delivery/deliveryLandingLazyRoute').then( (m) => m.deliveryLandingLazyRoute @@ -79,16 +122,16 @@ const destinationsRoute = createRoute({ ); const destinationsCreateRoute = createRoute({ - getParentRoute: () => deliveryRoute, - path: 'destinations/create', + getParentRoute: () => destinationsRoute, + path: 'create', }).lazy(() => import( 'src/features/Delivery/Destinations/DestinationForm/destinationCreateLazyRoute' ).then((m) => m.destinationCreateLazyRoute) ); -const destinationsEditRoute = createRoute({ - getParentRoute: () => deliveryRoute, +const destinationRoute = createRoute({ + getParentRoute: () => destinationsRoute, params: { parse: ({ destinationId }: { destinationId: string }) => ({ destinationId: Number(destinationId), @@ -97,7 +140,24 @@ const destinationsEditRoute = createRoute({ destinationId: String(destinationId), }), }, - path: 'destinations/$destinationId/edit', + path: '$destinationId', +}); + +const destinationLandingRoute = createRoute({ + beforeLoad: ({ params }) => { + throw redirect({ + to: '/logs/delivery/destinations/$destinationId/summary', + params: { destinationId: params.destinationId }, + replace: true, + }); + }, + getParentRoute: () => destinationRoute, + path: '/', +}); + +const destinationSummaryRoute = createRoute({ + getParentRoute: () => destinationRoute, + path: 'summary', }).lazy(() => import( 'src/features/Delivery/Destinations/DestinationForm/destinationEditLazyRoute' @@ -106,9 +166,21 @@ const destinationsEditRoute = createRoute({ export const deliveryRouteTree = deliveryRoute.addChildren([ deliveryLandingRoute, - streamsRoute.addChildren([streamsCreateRoute, streamsEditRoute]), + streamsRoute.addChildren([ + streamsLandingRoute, + streamsCreateRoute, + streamRoute.addChildren([ + streamLandingRoute, + streamSummaryRoute, + streamMetricsRoute, + ]), + ]), destinationsRoute.addChildren([ + destinationsLandingRoute, destinationsCreateRoute, - destinationsEditRoute, + destinationRoute.addChildren([ + destinationLandingRoute, + destinationSummaryRoute, + ]), ]), ]); diff --git a/packages/manager/src/store/selectors/getSearchEntities.ts b/packages/manager/src/store/selectors/getSearchEntities.ts index a6a30f838e8..47c7c20a85a 100644 --- a/packages/manager/src/store/selectors/getSearchEntities.ts +++ b/packages/manager/src/store/selectors/getSearchEntities.ts @@ -213,7 +213,7 @@ export const stackscriptToSearchableItem = ( export const streamToSearchableItem = (stream: Stream): SearchableItem => ({ data: { description: getStreamDescription(stream), - path: `/logs/delivery/streams/${stream.id}/edit`, + path: `/logs/delivery/streams/${stream.id}/summary`, status: stream.status, created: stream.created, }, @@ -227,7 +227,7 @@ export const destinationToSearchableItem = ( ): SearchableItem => ({ data: { description: getDestinationDescription(destination), - path: `/logs/delivery/destinations/${destination.id}/edit`, + path: `/logs/delivery/destinations/${destination.id}/summary`, created: destination.created, }, entityType: 'destination',