From cefbc2e3ceee4d76b53f72c57a86db849bd612b9 Mon Sep 17 00:00:00 2001 From: dweisz Date: Sun, 24 Nov 2024 14:20:23 +0200 Subject: [PATCH] external-api-genai-cognito-with-ad-docs-update --- .../.env.local.sample | 2 + .../src/app/auth/auth-service.ts | 4 +- .../src/app/cognito/aws-configs.ts | 1 + .../app/components/loginForm/loginForm.tsx | 51 +++--- .../src/app/global.module.scss | 6 +- .../src/app/home.tsx | 14 +- .../src/app/services/clerk.service.ts | 4 +- .../src/app/services/cognito.service.ts | 31 +++- .../src/lib/api/api.types.ts | 11 +- README.md | 157 +++++++++++------- 10 files changed, 178 insertions(+), 103 deletions(-) diff --git a/AI/GenAI-ChatBot-application-sample/.env.local.sample b/AI/GenAI-ChatBot-application-sample/.env.local.sample index 2a5b8cb..29b5798 100644 --- a/AI/GenAI-ChatBot-application-sample/.env.local.sample +++ b/AI/GenAI-ChatBot-application-sample/.env.local.sample @@ -2,6 +2,8 @@ NEXT_PUBLIC_LOGIN_PROVIDER=cognito NEXT_PUBLIC_KNOWLEDGE_BASE_ID=YOUR_KNOWLEDGE_BASE_ID NEXT_PUBLIC_AWS_USER_POOLS_ID=YOUR_AWS_USER_POOLS_ID NEXT_PUBLIC_AWS_USER_WEB_CLIENT_ID=YOUR_AWS_USER_WEB_CLIENT_ID +# NEXT_PUBLIC_LOGIN_EXTERNAL_PROVIDER=AD--NAME-SAMPLE +# NEXT_PUBLIC_AWS_OAUTH={"domain":".auth..amazoncognito.com","scope":["openid","profile","email"],"redirectSignIn":"http://localhost:9091","redirectSignOut":"http://localhost:9091","responseType":"code"} # NEXT_PUBLIC_LOGIN_PROVIDER=clerk # NEXT_PUBLIC_KNOWLEDGE_BASE_ID=YOUR_KNOWLEDGE_BASE_ID diff --git a/AI/GenAI-ChatBot-application-sample/src/app/auth/auth-service.ts b/AI/GenAI-ChatBot-application-sample/src/app/auth/auth-service.ts index 45e9d02..694c844 100644 --- a/AI/GenAI-ChatBot-application-sample/src/app/auth/auth-service.ts +++ b/AI/GenAI-ChatBot-application-sample/src/app/auth/auth-service.ts @@ -19,7 +19,7 @@ export class AuthService { await Auth.signUp({ username, password, - }).then(data => { + }).then(() => { logger.info("Registering " + username); @@ -131,7 +131,7 @@ export class AuthService { data: data }); }) - .catch(err => { + .catch(() => { logger.error("Couldn't sign out for some reason"); Hub.dispatch(AuthService.CHANNEL, { event: AuthService.AUTH_EVENTS.SIGN_OUT, diff --git a/AI/GenAI-ChatBot-application-sample/src/app/cognito/aws-configs.ts b/AI/GenAI-ChatBot-application-sample/src/app/cognito/aws-configs.ts index f082909..11f5ef7 100644 --- a/AI/GenAI-ChatBot-application-sample/src/app/cognito/aws-configs.ts +++ b/AI/GenAI-ChatBot-application-sample/src/app/cognito/aws-configs.ts @@ -5,6 +5,7 @@ const awsConfig = { aws_user_pools_mfa_type: 'OFF', aws_user_pools_web_client_id: process.env.NEXT_PUBLIC_AWS_USER_WEB_CLIENT_ID, aws_user_settings: 'enable', + oauth: process.env.NEXT_PUBLIC_AWS_OAUTH }; export default awsConfig diff --git a/AI/GenAI-ChatBot-application-sample/src/app/components/loginForm/loginForm.tsx b/AI/GenAI-ChatBot-application-sample/src/app/components/loginForm/loginForm.tsx index f958c53..4266ee9 100644 --- a/AI/GenAI-ChatBot-application-sample/src/app/components/loginForm/loginForm.tsx +++ b/AI/GenAI-ChatBot-application-sample/src/app/components/loginForm/loginForm.tsx @@ -15,11 +15,17 @@ import { addNotification, removeNotification } from '@/lib/slices/notifications. import { NOTIFICATION_CONSTS } from '../NotificationGroup/notification.consts'; import { setAuth } from '@/lib/slices/auth.slice'; import { ClerkSignIn } from '@/app/services/clerk.service'; -import { SignInResult, LoginProvider } from '@/lib/api/api.types'; +import {LoginProvider, SignInResultClerk, SignInResultCognito} from '@/lib/api/api.types'; import useRunUntil from '@/app/hooks/useRunUntil'; -const LoginForm = () => { +export type LoginType = 'AD' | 'UserPassword' +interface LoginFormProps { + onLoginSuccess: (loginType:LoginType )=> void +} + +const LoginForm = ({onLoginSuccess}:LoginFormProps) => { const loginProvider = process.env.NEXT_PUBLIC_LOGIN_PROVIDER as LoginProvider; + const loginEternalProvider:string | undefined = process.env.NEXT_PUBLIC_LOGIN_EXTERNAL_PROVIDER; const emailRef = useRef(null); @@ -29,8 +35,8 @@ const LoginForm = () => { const [email, setEmail] = useState(); const [password, setPassword] = useState(); - const { isLoading: isLoadingCog, jwtToken: jwtTokenCog, email: emailCog, password: passwordCog, userName: userNameCog, error: errorCog, doLogin: doLoginCog } = loginProvider === 'cognito' ? CognitoSignIn() : {} as SignInResult - const { isLoading: isLoadingClerk, jwtToken: jwtTokenClerk, email: emailClerk, password: passwordClerk, userName: userNameClerk, error: errorClerk, doLogin: doLoginClerk } = loginProvider === 'clerk' ? ClerkSignIn() : {} as SignInResult; + const { isLoading: isLoadingCog, jwtToken: jwtTokenCog, email: emailCog, password: passwordCog, userName: userNameCog, error: errorCog, doLogin: doLoginCog } = loginProvider === 'cognito' ? CognitoSignIn() : {} as SignInResultCognito + const { isLoading: isLoadingClerk, jwtToken: jwtTokenClerk, email: emailClerk, password: passwordClerk, userName: userNameClerk, error: errorClerk, doLogin: doLoginClerk } = loginProvider === 'clerk' ? ClerkSignIn() : {} as SignInResultClerk; useRunUntil(() => { emailRef.current?.focus(); @@ -53,6 +59,7 @@ const LoginForm = () => { useEffect(() => { if (jwtTokenCog || jwtTokenClerk) { + onLoginSuccess(loginEternalProvider? 'AD': 'UserPassword') dispatch(setAuth({ isSuccess: true, accessToken: jwtTokenCog || jwtTokenClerk, @@ -60,11 +67,11 @@ const LoginForm = () => { })); router.push(`${ROUTES.BASE}${ROUTES.CHAT}`); } - }, [jwtTokenCog, jwtTokenClerk, router, userNameClerk, userNameCog, dispatch]) + }, [jwtTokenCog, jwtTokenClerk, router, userNameClerk, userNameCog, dispatch, loginEternalProvider, onLoginSuccess]) const doLogin = () => { dispatch(removeNotification(NOTIFICATION_CONSTS.UNIQUE_IDS.USER_NOT_CONFIRMED)); - loginProvider === 'cognito' ? doLoginCog(email, password) : doLoginClerk(email, password); + loginProvider === 'cognito' ? doLoginCog(email, password,loginEternalProvider) : doLoginClerk(email, password); } return ( @@ -73,7 +80,8 @@ const LoginForm = () => {
Log in to Workload Factory GenAI sample application Log in to NetApp GenAI Studio chatbot sample application with
your company user account.
- + { value: 'This field is required.' } : undefined} /> - setPassword(event?.target.value)} - onKeyDown={event => { - if (event.key === 'Enter') { - doLogin(); - } - }} - message={password === '' ? { - type: 'error', - value: 'This field is required.' - } : undefined} /> + setPassword(event?.target.value)} + onKeyDown={event => { + if (event.key === 'Enter') { + doLogin(); + } + }} + message={password === '' ? { + type: 'error', + value: 'This field is required.' + } : undefined} /> + } doLogin()} className={styles.loginButton} isLoading={isLoadingCog || isLoadingClerk}>Login
diff --git a/AI/GenAI-ChatBot-application-sample/src/app/global.module.scss b/AI/GenAI-ChatBot-application-sample/src/app/global.module.scss index de164d7..dc0cad5 100644 --- a/AI/GenAI-ChatBot-application-sample/src/app/global.module.scss +++ b/AI/GenAI-ChatBot-application-sample/src/app/global.module.scss @@ -6,6 +6,10 @@ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; color: #404040; + &.isHidden { + opacity: 0; + } + &.font-variants { --semibold-font-weight: 500; @@ -37,7 +41,7 @@ font-size: 20px; line-height: 32px; font-weight: 400; - } + } .Regular_14 { font-size: 14px; diff --git a/AI/GenAI-ChatBot-application-sample/src/app/home.tsx b/AI/GenAI-ChatBot-application-sample/src/app/home.tsx index 7dcc098..19aade8 100644 --- a/AI/GenAI-ChatBot-application-sample/src/app/home.tsx +++ b/AI/GenAI-ChatBot-application-sample/src/app/home.tsx @@ -1,21 +1,25 @@ 'use client'; +import React from 'react'; import styles from './global.module.scss'; -import LoginForm from "./components/loginForm/loginForm"; +import LoginForm, { LoginType } from "./components/loginForm/loginForm"; import ChatBot from '@/app/svgs/login/chatBot.png' import Cloud from '@/app/svgs/login/cloud.png' import Image from 'next/image'; import { _Classes } from '@/utils/cssHelper.util'; import { useAppSelector } from '@/lib/hooks'; import rootSelector from '@/lib/selectors/root.selector'; +import { useState } from "react"; const Home = () => { const { accessToken, isSuccess } = useAppSelector(rootSelector.auth); - + const [loginType, setLoginType] = useState(undefined); return ( -
- {!accessToken && isSuccess && <> - +
+ {(!accessToken && isSuccess) && <> +
{ +export const ClerkSignIn = (): SignInResultClerk => { const [email, setEmail] = useState(); const [password, setPassword] = useState(); const [isLoading, setIsLoading] = useState(false); diff --git a/AI/GenAI-ChatBot-application-sample/src/app/services/cognito.service.ts b/AI/GenAI-ChatBot-application-sample/src/app/services/cognito.service.ts index 39b06b0..8d72678 100644 --- a/AI/GenAI-ChatBot-application-sample/src/app/services/cognito.service.ts +++ b/AI/GenAI-ChatBot-application-sample/src/app/services/cognito.service.ts @@ -3,10 +3,14 @@ import useRunOnce from "../hooks/useRunOnce"; import awsConfig from "../cognito/aws-configs"; import { useEffect, useState } from "react"; import { AuthService } from "../auth/auth-service"; -import { CognitoPayload } from "../cognito/cognito.types"; -import { SignInResult } from "@/lib/api/api.types"; +import {CognitoPayload} from "../cognito/cognito.types"; +import {SignInResultCognito} from "@/lib/api/api.types"; +import {CognitoUser} from "amazon-cognito-identity-js"; +import awsConfigs from "../cognito/aws-configs"; -const CognitoSignIn = (): SignInResult => { +const isLoginEternalProvider:boolean = !!process.env.NEXT_PUBLIC_LOGIN_EXTERNAL_PROVIDER; + +const CognitoSignIn = (): SignInResultCognito => { const [isLoading, setIsLoading] = useState(false); const [email, setEmail] = useState(); const [password, setPassword] = useState(); @@ -15,6 +19,7 @@ const CognitoSignIn = (): SignInResult => { const [userName, setUserName] = useState(); useRunOnce(() => { + if (awsConfig.oauth && typeof awsConfig.oauth === "string") awsConfigs.oauth = JSON.parse(awsConfigs.oauth!) Amplify.configure(awsConfig); }) @@ -52,7 +57,17 @@ const CognitoSignIn = (): SignInResult => { const updateUser = async () => { try { - await Auth.currentAuthenticatedUser() + const authenticatedUser:CognitoUser = await Auth.currentAuthenticatedUser() + if (isLoginEternalProvider){ + Hub.dispatch(AuthService.CHANNEL, { + event: AuthService.AUTH_EVENTS.LOGIN, + // @ts-ignore + success: true, + message: "", + username: authenticatedUser.getUsername(), + user: authenticatedUser + }); + } } catch { } } @@ -64,13 +79,17 @@ const CognitoSignIn = (): SignInResult => { }; }, []); - const doLogin = async (email?: string, password?: string) => { + const doLogin = async (email?: string, password?: string,externalProviderName?:string) => { setError(undefined); setEmail(email || ''); setPassword(password || ''); - if (email && password) { + if (externalProviderName){ + setIsLoading(true); + await Auth.federatedSignIn({ provider: externalProviderName as any }); + } + else if (email && password){ setIsLoading(true); await AuthService.login(email, password); } diff --git a/AI/GenAI-ChatBot-application-sample/src/lib/api/api.types.ts b/AI/GenAI-ChatBot-application-sample/src/lib/api/api.types.ts index afaf222..ebea8e4 100644 --- a/AI/GenAI-ChatBot-application-sample/src/lib/api/api.types.ts +++ b/AI/GenAI-ChatBot-application-sample/src/lib/api/api.types.ts @@ -5,11 +5,18 @@ export type ChunkingStrategy = 'sentences' | 'words' | 'characters'; export type MessageType = 'ANSWER' | 'ERROR'; export type LoginProvider = 'cognito' | 'clerk'; -export interface SignInResult { +export interface SignInResultCognito extends SignInResult { + doLogin: (email?: string, password?: string, externalProviderName?: string) => void, +} + +export interface SignInResultClerk extends SignInResult { + doLogin: (email?: string, password?: string) => void, +} + +interface SignInResult { isLoading: boolean, email?: string, password?: string, - doLogin: (email?: string, password?: string) => void, jwtToken?: string, error?: string, userName?: string diff --git a/README.md b/README.md index 85c8337..bb20dd0 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,93 @@ -# FSx-ONTAP-samples-scripts - -FSx for NetApp ONTAP is an AWS service providing a comprehensive set of advanced storage features purposely -built to maximize cost performance, resilience, and accessibility in business-critical workloads. - -## Overview - -This GitHub repository contains comprehensive code samples and automation scripts for FSx for Netapp ONTAP operations, -promoting the use of Infrastructure as Code (IAC) tools and encouraging developers to extend the product's -functionalities through code. The samples here go alongside the automation, management and monitoring that -[BlueXP Workload Factory](https://console.workloads.netapp.com) provides. - -We welcome contributions from the community! Please read our [contribution guidelines](CONTRIBUTING.md) before getting started. - -Have a great idea? We'd love to hear it! Please email us at [ng-fsxn-github-samples@netapp.com](mailto:ng-fsxn-github-samples@netapp.com). - -## Table of Contents - -* [AI](/AI) - * [GenAI ChatBot application sample](/AI/GenAI-ChatBot-application-sample) -* [Anisble](/Ansible) - * [FSx ONTAP inventory report](/Ansible/fsx_inventory_report) - * [SnapMirror report](/Ansible/snapmirror_report) -* [CloudFormation](/CloudFormation) - * [deploy-fsx-ontap](/CloudFormation/deploy-fsx-ontap) -* [EKS](/EKS) - * [FSx for NetApp ONTAP as persistent storage for EKS](/EKS/FSxN-as-PVC-for-EKS) -* [Management Utilities](/Management-Utilities) - * [Auto Create SnapMirror Relationships](/Management-Utilities/auto_create_sm_relationships) - * [Auto Set FSxN Auto Grow](/Management-Utilities/auto_set_fsxn_auto_grow) - * [AWS CLI management scripts for FSx ONTAP](/Management-Utilities/fsx-ontap-aws-cli-scripts) - * [Rotate AWS Secrets Manager Secret](/Management-Utilities/fsxn-rotate-secret) - * [FSx ONTAP iscsi volume creation automation for Windows](/Management-Utilities/iscsi-vol-create-and-mount) - * [Warm Performance Tier](/Management-Utilities/warm_performance_tier) -* [Monitoring](/Monitoring) - * [CloudWatch Dashboard for FSx for ONTAP](/Monitoring/CloudWatch-FSx) - * [Export LUN metrics from an FSx ONTAP to Amazon CloudWatch](/Monitoring/LUN-monitoring) - * [Automatically Add CloudWatch Alarms for FSx Resources](/Monitoring/auto-add-cw-alarms) - * [Monitor ONTAP metrics from FSx ONTAP using python Lambda function](/Monitoring/monitor-ontap-services) - * [Monitor FSx for ONTAP with Harvest on EKS](/Monitoring/monitor_fsxn_with_harvest_on_eks) -* [Solutions](/Solutions) - * [k8s applications non-stdout logs collection into ELK](/Solutions/EKS-logs-to-ELK) -* [Terraform](/Terraform) - * [FSx ONTAP deployment using Terraform](/Terraform/deploy-fsx-ontap) - * [FSx ONTAP Replication](/Terraform/fsxn-replicate) - * [Deployment of SQL Server on EC2 with FSx ONTAP](/Terraform/deploy-fsx-ontap-sqlserver) - * [Deployment of FSx ONTAP with VPN for File Share Access](/Terraform/deploy-fsx-ontap-fileshare-access) - -## Author Information - -This repository is maintained by the contributors listed on [GitHub](https://github.com/NetApp/FSx-ONTAP-samples-scripts/graphs/contributors). - -## License - -Licensed under the Apache License, Version 2.0 (the "License"). - -You may obtain a copy of the License at [apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). - -Unless required by applicable law or agreed to in writing, software distributed under the License -is distributed on an _"AS IS"_ basis, without WARRANTIES or conditions of any kind, either express or implied. - -See the License for the specific language governing permissions and limitations under the License. - -© 2024 NetApp, Inc. All Rights Reserved. +# NetApp Workload Factory GenAI sample application + +## Introduction +The NetApp Workload Factory GenAI sample application enables external application developers to test authentication and retrieval from a published NetApp Workload Factory knowledge base by interacting directly with it in a web-based chatbot application. Its features are similar to the chatbot interface within the NetApp Workload Factory UI, and it uses the same Workload Factory API for conversations. As a developer, you can use this sample application to test published knowledge bases and see API examples that can help you develop your own chatbot application. + +## Application components +The NetApp Workload Factory GenAI sample application is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +The sample application uses [Redux Toolkit](https://redux-toolkit.js.org) with [RTK Query](https://redux-toolkit.js.org/tutorials/rtk-query) for data fetching. + +## Requirements +- [Node.js](https://nodejs.org/) 18.17 or later stable version. +- The NetApp Workload Factory GenAI sample application relies on one of the following login providers: + - [Amazon Cognito](https://aws.amazon.com/cognito/) + [Amazon Amplify Framework](https://aws-amplify.github.io/docs/js/start) + - [Clerk](https://clerk.com/) +- You need a knowledge base created with NetApp Workload Factory GenAI that is configured for active authentication and published: + - [Activate external authentication for a knowledge base](https://docs.netapp.com/us-en/workload-genai/activate-authentication.html) + - [Publish a knowledge base](https://docs.netapp.com/us-en/workload-genai/publish-knowledgebase.html) +- You need the ID of the published knowledge base. You can find the knowledge base ID on the **Knowledge bases > Manage knowledge base** page in Workload Factory GenAI, or you can work with the person that created the knowledge base. + +## Set up login providers +To get started, you need to configure one of the supported login providers. Configure the same login provider (issuer) that is used by the knowledge base that will integrate with the sample chatbot application. + +### Set up AWS Cognito +1. Follow the [Create user pool](https://www.cognitobuilders.training/20-lab1/20-setup-and-explore/10-create-userpool/) instructions to create a Cognito user pool. +2. Use the Amazon Cognito documentation to find your user pool ID and user web client ID: + 1. [Find your user pool ID](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cognito-idp/list-user-pools.html). + For example: `aws cognito-idp list-user-pools --max-results=60 --output=table` + 2. [Find your web client ID](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cognito-idp/list-user-pool-clients.html). + For example: `aws cognito-idp list-user-pool-clients --user-pool-id $(USER_POOL_ID) --output=table` +3. Download and unpack the Workload Factory GenAI sample application source package. +4. In the Workload Factory GenAI sample application source, rename the `.env.local.sample` file to `.env.local`. +5. In the `.env.local` file, uncomment the corresponding section for the login provider you plan to use, and make sure the section for the other provider is commented out. +6. In the `.env.local` file, change the following variables in the appropriate provider section to match your environment. Replace `YOUR_KNOWLEDGE_BASE_ID` with the knowledge base ID from Workload Factory: + - NEXT_PUBLIC_LOGIN_PROVIDER=cognito + - NEXT_PUBLIC_KNOWLEDGE_BASE_ID=YOUR_KNOWLEDGE_BASE_ID + - NEXT_PUBLIC_AWS_USER_POOLS_ID=YOUR_AWS_USER_POOLS_ID + - NEXT_PUBLIC_AWS_USER_WEB_CLIENT_ID=YOUR_AWS_USER_WEB_CLIENT_ID + +#### Configure Active Directory with Amazon Cognito +1. To integrate Active Directory with Amazon Cognito, follow the [A guide to AD FS federation with Amazon Cognito user pools instructions (Steps 1 to 4)](https://aws.amazon.com/blogs/security/simplify-web-app-authentication-a-guide-to-ad-fs-federation-with-amazon-cognito-user-pools/). +2. To configure SAML request signing, follow the instructions in [Signing SAML requests](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-SAML-signing-encryption.html#cognito-user-pools-SAML-signing). +3. Specify the federated identity provider and authorization by adding the following variables to the `.env.local` file, with values that match your environment. + - NEXT_PUBLIC_LOGIN_EXTERNAL_PROVIDER=YOUR_COGNITO_IDENTITY_PROVIDER_NAME + - NEXT_PUBLIC_AWS_OAUTH={"domain":".auth..amazoncognito.com","scope":["openid", "profile", "email"],"redirectSignIn":"http://localhost:9091","redirectSignOut":"http://localhost:9091","responseType":"code"} + - Domain - The domain for your Cognito user pool. + - Scope - Scopes specifying the access privileges. + - redirectSignIn - The URL to redirect to after a successful sign-in. + - redirectSignOut - The URL to redirect to after a successful sign-out. + - responseType - The type of response to receive from the authorization server. + +### Set up Clerk +1. Follow the [Sign up](https://dashboard.clerk.com/sign-in?redirect_url=https%3A%2F%2Fdashboard.clerk.com%2F) instructions to sign up for a Clerk account. +2. Download and unpack the Workload Factory GenAI sample application source package. +3. In the Workload Factory GenAI sample application source, rename the `.env.local.sample` file to `.env.local`. +4. In the `.env.local` file, uncomment the corresponding section for the login provider you plan to use, and make sure the section for the other provider is commented out. +5. In the `.env.local` file, change the following variables in the appropriate provider section to match your environment. Replace `YOUR_KNOWLEDGE_BASE_ID` with the knowledge base ID from Workload Factory: + - NEXT_PUBLIC_LOGIN_PROVIDER=clerk + - NEXT_PUBLIC_KNOWLEDGE_BASE_ID=YOUR_KNOWLEDGE_BASE_ID + - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=YOUR_NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY + - CLERK_SECRET_KEY=YOUR_CLERK_SECRET_KEY + - NEXT_PUBLIC_CLERK_TEMPLATE=YOUR_CLERK_TEMPLATE + +## Install the application +To install the sample application, run the following command: + +```bash +npm install +``` + +## Run the application +1. To run the application locally, run the following command: + + ```bash + npm run dev + ``` + +2. Open [http://localhost:9091](http://localhost:9091) with your browser to log in to the application. + +## Build the application +To build bundle.js, run the following command: + +```bash +npm run build +``` + +## Learn More + +- Learn more about [BlueXP Workload Factory for AWS](https://docs.netapp.com/us-en/workload-genai/index.html). +- Learn more about the APIs used in this sample application by visiting the [Workload Factory API documentation](https://console.workloads.netapp.com/api-doc). +- To learn more about Next.js, take a look at the following resources: + - [Next.js documentation](https://nextjs.org/docs) - learn about Next.js features and API. + - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. \ No newline at end of file