diff --git a/workspaces/frontend/src/__tests__/cypress/.gitignore b/workspaces/frontend/src/__tests__/cypress/.gitignore index c1f053610..4539e50c0 100644 --- a/workspaces/frontend/src/__tests__/cypress/.gitignore +++ b/workspaces/frontend/src/__tests__/cypress/.gitignore @@ -1,2 +1,3 @@ coverage results +cypress/downloads/ \ No newline at end of file diff --git a/workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/WorkspaceDetails/WorkspaceDetailsActivity.cy.ts b/workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/WorkspaceDetails/WorkspaceDetailsActivity.cy.ts new file mode 100644 index 000000000..624d8cc00 --- /dev/null +++ b/workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/WorkspaceDetails/WorkspaceDetailsActivity.cy.ts @@ -0,0 +1,18 @@ +import { home } from '~/__tests__/cypress/cypress/pages/home'; + +describe('WorkspaceDetailsActivity Component', () => { + beforeEach(() => { + home.visit(); + }); + + // This tests depends on the mocked workspaces data at home page, needs revisit once workspace data fetched from BE + it('open workspace details, open activity tab, check all fields match', () => { + cy.findAllByTestId('table-body').first().findByTestId('action-column').click(); + cy.findByTestId('action-view-details').click(); + cy.findByTestId('activityTab').click(); + cy.findByTestId('lastActivity').should('have.text', '2/16/2025, 4:40:00 AM'); + cy.findByTestId('lastUpdate').should('have.text', '2/16/2025, 4:41:40 AM'); + cy.findByTestId('pauseTime').should('have.text', '2/16/2025, 4:38:20 AM'); + cy.findByTestId('pendingRestart').should('have.text', 'No'); + }); +}); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx index c71a34be6..2c794894f 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx @@ -15,6 +15,7 @@ import { import { Workspace } from '~/shared/types'; import { WorkspaceDetailsOverview } from '~/app/pages/Workspaces/Details/WorkspaceDetailsOverview'; import { WorkspaceDetailsActions } from '~/app/pages/Workspaces/Details/WorkspaceDetailsActions'; +import { WorkspaceDetailsActivity } from '~/app/pages/Workspaces/Details/WorkspaceDetailsActivity'; type WorkspaceDetailsProps = { workspace: Workspace; @@ -39,7 +40,7 @@ export const WorkspaceDetails: React.FunctionComponent = }; return ( - + {workspace.name} @@ -56,9 +57,16 @@ export const WorkspaceDetails: React.FunctionComponent = - Activity} aria-label="Activity"> + Activity} + aria-label="Activity" + data-testid="activityTab" + > - Activity + + + Logs} aria-label="Logs"> diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx new file mode 100644 index 000000000..5832d2a3e --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { + DescriptionList, + DescriptionListTerm, + DescriptionListGroup, + DescriptionListDescription, + Divider, +} from '@patternfly/react-core'; +import { Workspace } from '~/shared/types'; +import { formatTimestamp } from '~/shared/utilities/WorkspaceUtils'; + +type WorkspaceDetailsActivityProps = { + workspace: Workspace; +}; + +export const WorkspaceDetailsActivity: React.FunctionComponent = ({ + workspace, +}) => { + const { activity, pauseTime, pendingRestart } = workspace.status; + + return ( + + + Last Activity + + {formatTimestamp(activity.lastActivity)} + + + + + Last Update + + {formatTimestamp(activity.lastUpdate)} + + + + + Pause Time + + {formatTimestamp(pauseTime)} + + + + + Pending Restart + + {pendingRestart ? 'Yes' : 'No'} + + + + + ); +}; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index d19dd6add..f8c17a065 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -47,7 +47,7 @@ import { WorkspaceStartActionModal } from '~/app/pages/Workspaces/workspaceActio import { WorkspaceRestartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal'; import { WorkspaceStopActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal'; import Filter, { FilteredColumn } from 'shared/components/Filter'; -import { formatRam } from 'shared/utilities/WorkspaceResources'; +import { formatRam } from 'shared/utilities/WorkspaceUtils'; export enum ActionType { ViewDetails, @@ -102,11 +102,11 @@ export const Workspaces: React.FunctionComponent = () => { }, status: { activity: { - lastActivity: 0, - lastUpdate: 0, + lastActivity: 1739673600, + lastUpdate: 1739673700, }, - pauseTime: 0, - pendingRestart: true, + pauseTime: 1739673500, + pendingRestart: false, podTemplateOptions: { imageConfig: { desired: '', @@ -384,14 +384,17 @@ export const Workspaces: React.FunctionComponent = () => { const workspaceState = workspace.status.state; const workspaceActions = [ { + id: 'view-details', title: 'View Details', onClick: () => viewDetailsClick(workspace), }, { + id: 'edit', title: 'Edit', onClick: () => editAction(workspace), }, { + id: 'delete', title: 'Delete', onClick: () => handleDeleteClick(workspace), }, @@ -400,10 +403,12 @@ export const Workspaces: React.FunctionComponent = () => { }, workspaceState !== WorkspaceState.Running ? { + id: 'start', title: 'Start', onClick: () => startRestartAction(workspace, ActionType.Start), } : { + id: 'restart', title: 'Restart', onClick: () => startRestartAction(workspace, ActionType.Restart), }, @@ -411,6 +416,7 @@ export const Workspaces: React.FunctionComponent = () => { if (workspaceState === WorkspaceState.Running) { workspaceActions.push({ + id: 'stop', title: 'Stop', onClick: () => stopAction(workspace), }); @@ -633,7 +639,7 @@ export const Workspaces: React.FunctionComponent = () => { ({ ...action, - 'data-testid': `action-${typeof action.title === 'string' ? action.title.toLowerCase() : ''}`, + 'data-testid': `action-${action.id || ''}`, }))} /> diff --git a/workspaces/frontend/src/shared/utilities/WorkspaceResources.ts b/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts similarity index 60% rename from workspaces/frontend/src/shared/utilities/WorkspaceResources.ts rename to workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts index ed8ce6d48..f89fa221a 100644 --- a/workspaces/frontend/src/shared/utilities/WorkspaceResources.ts +++ b/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts @@ -10,3 +10,7 @@ export const formatRam = (valueInKb: number): string => { return `${value.toFixed(2)} ${units[index]}`; }; + +// Helper function to format UNIX timestamps +export const formatTimestamp = (timestamp: number): string => + timestamp && timestamp > 0 ? new Date(timestamp * 1000).toLocaleString() : '-';