Skip to content

Commit dc372c4

Browse files
spkambojchungjac
andauthored
feat(sagemakerunifiedstudio): Fetch notebook training jobs (aws#2184)
## Problem Need backend logic to fetch notebook training jobs for rendering pages ## Solution - Add SageMaker client for fetching notebook training jobs - Call SageMaker client in extension host, where it has access to project role credentials for making API calls - Add backend logic to transfer jobs between frontend and backend - Use Vue composable to store fetched data for reactive state rendering - Update JobList page to consume fetched data from composable to render page - Update JobDetail page to consume fetched data from composable to render job details #### TODO - Based on comments, move time formatting related utils to shared usage #### List jobs page consuming fetched data for rendering jobs list <img width="1372" height="1152" alt="Screenshot 2025-08-05 at 10 25 29 AM" src="https://github.com/user-attachments/assets/c94277ac-024b-41e7-868d-a025a5f20b31" /> #### Job detail page using fetch jobs data for rendering job details <img width="1372" height="1267" alt="Screenshot 2025-08-05 at 10 25 47 AM" src="https://github.com/user-attachments/assets/d6c1f54c-7e31-4339-9794-de307cb3c7d1" /> --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: chungjac <[email protected]>
1 parent e68d5ae commit dc372c4

File tree

16 files changed

+634
-2080
lines changed

16 files changed

+634
-2080
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ src.gen/*
3232
**/src/codewhisperer/client/codewhispererclient.d.ts
3333
**/src/codewhisperer/client/codewhispereruserclient.d.ts
3434
**/src/auth/sso/oidcclientpkce.d.ts
35+
**/src/sagemakerunifiedstudio/shared/client/gluecatalogapi.d.ts
36+
**/src/sagemakerunifiedstudio/shared/client/sqlworkbench.d.ts
3537

3638
# Generated by tests
3739
**/src/testFixtures/**/bin

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/src/sagemakerunifiedstudio/notebookScheduling/activation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ async function createWebview(context: vscode.ExtensionContext, page: Page): Prom
6767
viewColumn: vscode.ViewColumn.Active,
6868
})
6969

70+
// This is temporary setup for now.
71+
activePanel.server.initSdkClient('us-east-2', 'd95hwmylut2ai1')
7072
activePanel.server.setWebviewPanel(webviewPanel)
7173
activePanel.server.setCurrentPage(page)
7274

packages/core/src/sagemakerunifiedstudio/notebookScheduling/backend/notebookJobWebview.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as vscode from 'vscode'
77
import { VueWebview } from '../../../webviews/main'
88
import { createJobPage, Page } from '../utils/constants'
9+
import { SageMakerClient, ListJobsResponse } from '../client/sageMakerClient'
910

1011
/**
1112
* Webview class for managing SageMaker notebook job scheduling UI.
@@ -27,13 +28,19 @@ export class NotebookJobWebview extends VueWebview {
2728
/** Tracks the currently displayed page */
2829
private currentPage: Page = { name: createJobPage, metadata: {} }
2930

31+
private sageMakerClient?: SageMakerClient
32+
3033
/**
3134
* Creates a new NotebookJobWebview instance
3235
*/
3336
public constructor() {
3437
super(NotebookJobWebview.sourcePath)
3538
}
3639

40+
public initSdkClient(regionCode: string, projectId: string): void {
41+
this.sageMakerClient = new SageMakerClient(regionCode, projectId)
42+
}
43+
3744
public setWebviewPanel(newWebviewPanel: vscode.WebviewPanel): void {
3845
this.webviewPanel = newWebviewPanel
3946
}
@@ -54,4 +61,12 @@ export class NotebookJobWebview extends VueWebview {
5461
this.currentPage = newPage
5562
this.onShowPage.fire({ page: this.currentPage })
5663
}
64+
65+
public async listJobs(): Promise<ListJobsResponse> {
66+
if (!this.sageMakerClient) {
67+
throw 'SageMakerClient is not initialized'
68+
}
69+
70+
return this.sageMakerClient.listJobs()
71+
}
5772
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import {
7+
SageMakerClient as SageMakerClientSDK,
8+
SearchCommandInput,
9+
paginateSearch,
10+
TrainingJob,
11+
} from '@aws-sdk/client-sagemaker'
12+
import { ClientWrapper } from '../../../shared/clients/clientWrapper'
13+
import { getLogger } from '../../../shared/logger/logger'
14+
import { SageMakerSearchSortOrder, JobTag } from '../utils/constants'
15+
16+
export interface ListJobsResponse {
17+
jobs?: TrainingJob[]
18+
error?: string
19+
}
20+
21+
export class SageMakerClient extends ClientWrapper<SageMakerClientSDK> {
22+
private projectId: string
23+
24+
public constructor(regionCode: string, projectId: string) {
25+
super(regionCode, SageMakerClientSDK)
26+
27+
this.projectId = projectId
28+
}
29+
30+
public async listJobs(): Promise<ListJobsResponse> {
31+
const searchRequest: SearchCommandInput = {
32+
MaxResults: 100,
33+
Resource: 'TrainingJob',
34+
SortBy: 'CreationTime',
35+
SortOrder: SageMakerSearchSortOrder.DESCENDING,
36+
SearchExpression: {
37+
Filters: [
38+
{
39+
Name: `Tags.${JobTag.IS_STUDIO_ARCHIVED}`,
40+
Operator: 'Equals',
41+
Value: 'false',
42+
},
43+
],
44+
SubExpressions: [
45+
{
46+
Filters: [
47+
{
48+
Name: `Tags.${JobTag.IS_SCHEDULING_NOTEBOOK_JOB}`,
49+
Operator: 'Equals',
50+
Value: 'true',
51+
},
52+
{
53+
Name: `Tags.${JobTag.NOTEBOOK_JOB_ORIGIN}`,
54+
Operator: 'Equals',
55+
Value: 'PIPELINE_STEP',
56+
},
57+
],
58+
Operator: 'Or',
59+
},
60+
],
61+
},
62+
VisibilityConditions: [
63+
{
64+
Key: `Tags.${JobTag.AmazonDataZoneProject}`,
65+
Value: this.projectId,
66+
},
67+
],
68+
}
69+
70+
try {
71+
const jobs: TrainingJob[] = []
72+
const paginator = paginateSearch({ client: this.getClient() }, searchRequest)
73+
74+
for await (const page of paginator) {
75+
for (const result of page.Results ?? []) {
76+
if (result.TrainingJob) {
77+
jobs.push(result.TrainingJob)
78+
}
79+
}
80+
}
81+
82+
return { jobs }
83+
} catch (error: any) {
84+
getLogger().error('SageMakerClient.listJobs: %s', error)
85+
return { error: `${error}` }
86+
}
87+
}
88+
}

packages/core/src/sagemakerunifiedstudio/notebookScheduling/utils/constants.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,68 @@ export const jobDetailPage: string = 'jobDetailPage'
4242
export const jobDefinitionDetailPage: string = 'jobDefinitionDetailPage'
4343

4444
export const editJobDefinitionPage: string = 'editJobDefinitionPage'
45+
46+
export enum SageMakerSearchSortOrder {
47+
ASCENDING = 'Ascending',
48+
DESCENDING = 'Descending',
49+
}
50+
51+
export enum SearchSortOrder {
52+
ASCENDING = 'Ascending',
53+
DESCENDING = 'Descending',
54+
NONE = 'None',
55+
}
56+
57+
export enum JobTag {
58+
IS_SCHEDULING_NOTEBOOK_JOB = 'sagemaker:is-scheduling-notebook-job',
59+
IS_STUDIO_ARCHIVED = 'sagemaker:is-studio-archived',
60+
JOB_DEFINITION_ID = 'sagemaker:job-definition-id',
61+
NAME = 'sagemaker:name',
62+
NOTEBOOK_NAME = 'sagemaker:notebook-name',
63+
USER_PROFILE_NAME = 'sagemaker:user-profile-name',
64+
NOTEBOOK_JOB_ORIGIN = 'sagemaker:notebook-job-origin',
65+
AmazonDataZoneProject = 'AmazonDataZoneProject',
66+
ONE_TIME_SCHEDULE = 'sagemaker-studio:one-time',
67+
SMUS_USER_ID = 'sagemaker-studio:user-id',
68+
}
69+
70+
export enum RuntimeEnvironmentParameterName {
71+
SM_IMAGE = 'sm_image',
72+
SM_KERNEL = 'sm_kernel',
73+
SM_INIT_SCRIPT = 'sm_init_script',
74+
SM_LCC_INIT_SCRIPT_ARN = 'sm_lcc_init_script_arn',
75+
S3_INPUT = 's3_input',
76+
S3_INPUT_ACCOUNT_ID = 's3_input_account_id',
77+
S3_OUTPUT = 's3_output',
78+
S3_OUTPUT_ACCOUNT_ID = 's3_output_account_id',
79+
ROLE_ARN = 'role_arn',
80+
VPC_SECURITY_GROUP_IDS = 'vpc_security_group_ids',
81+
VPC_SUBNETS = 'vpc_subnets',
82+
SM_OUTPUT_KMS_KEY = 'sm_output_kms_key',
83+
SM_VOLUME_KMS_KEY = 'sm_volume_kms_key',
84+
MAX_RETRY_ATTEMPTS = 'max_retry_attempts',
85+
MAX_RUN_TIME_IN_SECONDS = 'max_run_time_in_seconds',
86+
SM_SKIP_EFS_SIMULATION = 'sm_skip_efs_simulation',
87+
ENABLE_NETWORK_ISOLATION = 'enable_network_isolation',
88+
}
89+
90+
export enum JobEnvironmentVariableName {
91+
SM_JOB_DEF_VERSION = 'SM_JOB_DEF_VERSION',
92+
SM_FIRST_PARTY_IMAGEOWNER = 'SM_FIRST_PARTY_IMAGEOWNER',
93+
SM_FIRST_PARTY_IMAGE_ARN = 'SM_FIRST_PARTY_IMAGE_ARN',
94+
SM_KERNEL_NAME = 'SM_KERNEL_NAME',
95+
SM_SKIP_EFS_SIMULATION = 'SM_SKIP_EFS_SIMULATION',
96+
SM_EFS_MOUNT_PATH = 'SM_EFS_MOUNT_PATH',
97+
SM_EFS_MOUNT_UID = 'SM_EFS_MOUNT_UID',
98+
SM_EFS_MOUNT_GID = 'SM_EFS_MOUNT_GID',
99+
SM_INPUT_NOTEBOOK_NAME = 'SM_INPUT_NOTEBOOK_NAME',
100+
SM_OUTPUT_NOTEBOOK_NAME = 'SM_OUTPUT_NOTEBOOK_NAME',
101+
AWS_DEFAULT_REGION = 'AWS_DEFAULT_REGION',
102+
SM_ENV_NAME = 'SM_ENV_NAME',
103+
SM_INIT_SCRIPT = 'SM_INIT_SCRIPT',
104+
SM_LCC_INIT_SCRIPT = 'SM_LCC_INIT_SCRIPT',
105+
SM_LCC_INIT_SCRIPT_ARN = 'SM_LCC_INIT_SCRIPT_ARN',
106+
SM_OUTPUT_FORMATS = 'SM_OUTPUT_FORMATS',
107+
SM_EXECUTION_INPUT_PATH = 'SM_EXECUTION_INPUT_PATH',
108+
SM_PACKAGE_INPUT_FOLDER = 'SM_PACKAGE_INPUT_FOLDER',
109+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { TrainingJob, TrainingJobStatus } from '@aws-sdk/client-sagemaker'
7+
import { RuntimeEnvironmentParameterName, JobEnvironmentVariableName, JobTag } from './constants'
8+
9+
export function getJobName(job: TrainingJob): string | undefined {
10+
if (job.Tags) {
11+
for (const { Key, Value } of job.Tags) {
12+
if (Key === JobTag.NAME) {
13+
return Value
14+
}
15+
}
16+
}
17+
}
18+
19+
export function getJobStatus(job: TrainingJob) {
20+
if (job.TrainingJobStatus === TrainingJobStatus.IN_PROGRESS) {
21+
return 'In progress'
22+
}
23+
24+
return job.TrainingJobStatus
25+
}
26+
27+
export function getJobInputNotebookName(job: TrainingJob): string | undefined {
28+
if (job.Environment) {
29+
return job.Environment[JobEnvironmentVariableName.SM_INPUT_NOTEBOOK_NAME]
30+
}
31+
}
32+
33+
export function getJobEnvironmentName(job: TrainingJob): string | undefined {
34+
if (job.Environment) {
35+
return job.Environment[JobEnvironmentVariableName.SM_ENV_NAME]
36+
}
37+
}
38+
39+
export function getJobKernelName(job: TrainingJob): string | undefined {
40+
if (job.Environment) {
41+
return job.Environment[JobEnvironmentVariableName.SM_KERNEL_NAME]
42+
}
43+
}
44+
45+
export function getJobParameters(job: TrainingJob): { key: string; value: string }[] | undefined {
46+
if (job.HyperParameters) {
47+
const result = []
48+
49+
for (const [key, value] of Object.entries(job.HyperParameters)) {
50+
result.push({ key, value })
51+
}
52+
53+
return result
54+
}
55+
}
56+
57+
export function getJobEnvironmentParameters(job: TrainingJob) {
58+
if (job.Environment) {
59+
const runtimeEnvironmentParameterSet: Set<string> = new Set(Object.values(RuntimeEnvironmentParameterName))
60+
const jobEnvironmentVariableSet: Set<string> = new Set(Object.values(JobEnvironmentVariableName))
61+
62+
const result = []
63+
64+
for (const [key, value] of Object.entries(job.Environment)) {
65+
if (!(runtimeEnvironmentParameterSet.has(key) || jobEnvironmentVariableSet.has(key))) {
66+
result.push({ key, value })
67+
}
68+
}
69+
70+
return result
71+
}
72+
}
73+
74+
export function getJobRanWithInputFolder(job: TrainingJob): boolean {
75+
if (job.Environment && JobEnvironmentVariableName.SM_PACKAGE_INPUT_FOLDER in job.Environment) {
76+
return job.Environment[JobEnvironmentVariableName.SM_PACKAGE_INPUT_FOLDER].toLowerCase() === 'true'
77+
}
78+
79+
return false
80+
}
81+
82+
export function getFormattedDateTime(utcString: string): string {
83+
const date = new Date(utcString)
84+
85+
const pad = (n: number) => n.toString().padStart(2, '0')
86+
87+
const year = date.getFullYear()
88+
const month = pad(date.getMonth() + 1)
89+
const day = pad(date.getDate())
90+
91+
let hours = date.getHours()
92+
const minutes = pad(date.getMinutes())
93+
94+
// Determine AM or PM
95+
const ampm = hours >= 12 ? 'PM' : 'AM'
96+
97+
// Convert to 12-hour format
98+
hours = hours % 12
99+
hours = hours ? hours : 12 // 0 should be 12
100+
101+
const formatted = `${year}-${month}-${day}, ${pad(hours)}:${minutes} ${ampm}`
102+
return formatted
103+
}
104+
105+
export function getFormattedCurrentTime(): string {
106+
const date = new Date()
107+
108+
const pad = (n: number) => n.toString().padStart(2, '0')
109+
110+
let hours = date.getHours()
111+
const minutes = pad(date.getMinutes())
112+
const seconds = pad(date.getSeconds())
113+
114+
// Determine AM or PM
115+
const ampm = hours >= 12 ? 'PM' : 'AM'
116+
117+
// Convert to 12-hour format
118+
hours = hours % 12
119+
hours = hours ? hours : 12 // 0 should be 12
120+
121+
const formatted = `${pad(hours)}:${minutes}:${seconds} ${ampm}`
122+
return formatted
123+
}

0 commit comments

Comments
 (0)