diff --git a/.github/workflows/push-to-master-disconnected.yaml b/.github/workflows/push-to-master-disconnected.yaml new file mode 100644 index 0000000000..44ece386a7 --- /dev/null +++ b/.github/workflows/push-to-master-disconnected.yaml @@ -0,0 +1,92 @@ +name: Assisted Disconnected UI - Push to master or release tag + +on: + push: + branches: + - master + tags: + - 'v*-dis' + +env: + QUAY_ORG: quay.io/edge-infrastructure + QUAY_REPO: assisted-disconnected-ui + +jobs: + preflight-check: + # Prevents running the workflow when a brand-new tag points to the same commit as the master branch + runs-on: ubuntu-latest + outputs: + skip: ${{ steps.check.outputs.skip }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Check if a tag points to the same commit as the master branch + id: check + run: | + if [[ "${GITHUB_REF_TYPE}" == "tag" ]] && [[ "${GITHUB_SHA}" == "$(git rev-parse origin/master)" ]]; then + skip=true + else + skip=false + fi + echo "skip=${skip}" >> $GITHUB_OUTPUT + echo "skip=${skip}" + + publish-assisted-disconnected-ui: + needs: preflight-check + if: needs.preflight-check.outputs.skip == 'false' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Generate AIUI_APP_VERSION + run: | + AIUI_APP_VERSION=latest + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + AIUI_APP_VERSION=${GITHUB_REF_NAME} + fi + echo "AIUI_APP_VERSION=${AIUI_APP_VERSION}" >> $GITHUB_ENV + echo "AIUI_APP_VERSION=${AIUI_APP_VERSION}" + + - name: Generate tags + id: generate-tags + run: | + tags=( latest-${GITHUB_SHA} latest ) + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + tags+=( ${GITHUB_REF_NAME} ) + if [[ ! "${GITHUB_REF_NAME}" =~ -cim$ ]]; then + tags+=( staging-released ) + fi + fi + echo "tags=${tags[@]}" >> $GITHUB_OUTPUT + echo "tags=${tags[@]}" + + - name: Build + id: build + uses: redhat-actions/buildah-build@v2 + with: + image: ${{ env.QUAY_REPO }} + tags: ${{ steps.generate-tags.outputs.tags }} + labels: | + org.openshift-assisted.github.repository=${{ github.repository }} + org.openshift-assisted.github.actor=${{ github.actor }} + org.openshift-assisted.github.run_id=${{ github.run_id }} + org.openshift-assisted.github.sha=${{ github.sha }} + org.openshift-assisted.github.ref_name=${{ github.ref_name }} + build-args: | + AIUI_APP_VERSION=${{ env.AIUI_APP_VERSION }} + AIUI_APP_GIT_SHA=${{ github.sha }} + AIUI_APP_IMAGE_REPO=${{ env.QUAY_ORG }}/${{ env.QUAY_REPO }} + containerfiles: apps/assisted-disconnected-ui/Containerfile + context: . + + - name: Push to Quay.io + id: push + uses: redhat-actions/push-to-registry@v2.7 + with: + image: ${{ steps.build.outputs.image }} + tags: ${{ steps.build.outputs.tags }} + registry: ${{ env.QUAY_ORG }} + username: ${{ secrets.QUAY_EDGE_INFRA_ROBOT_USERNAME }} + password: ${{ secrets.QUAY_EDGE_INFRA_ROBOT_PASSWORD }} diff --git a/.github/workflows/push-to-master.yaml b/.github/workflows/push-to-master.yaml index 8623549d31..83b5118fe5 100644 --- a/.github/workflows/push-to-master.yaml +++ b/.github/workflows/push-to-master.yaml @@ -7,6 +7,7 @@ on: tags: - 'v*' - '!v*-cim' + - '!v*-virt' env: QUAY_ORG: quay.io/edge-infrastructure diff --git a/.gitignore b/.gitignore index 3dbe2256f0..795c698c66 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ package-lock.json # Ignored apps apps/* !apps/assisted-ui +!apps/assisted-disconnected-ui diff --git a/apps/assisted-disconnected-ui/.containerignore b/apps/assisted-disconnected-ui/.containerignore new file mode 100644 index 0000000000..89aa5bb598 --- /dev/null +++ b/apps/assisted-disconnected-ui/.containerignore @@ -0,0 +1,2 @@ +**/node_modules/ +**/build/ \ No newline at end of file diff --git a/apps/assisted-disconnected-ui/.eslintrc.cjs b/apps/assisted-disconnected-ui/.eslintrc.cjs new file mode 100644 index 0000000000..e232428fc5 --- /dev/null +++ b/apps/assisted-disconnected-ui/.eslintrc.cjs @@ -0,0 +1,55 @@ +/** @type {import('eslint').ESLint.ConfigData} */ +module.exports = { + overrides: [ + { + files: ['./vite.config.ts'], + extends: ['@openshift-assisted/eslint-config'], + env: { + browser: false, + }, + parserOptions: { + tsconfigRootDir: __dirname, + }, + rules: { + 'no-console': 'off', + }, + }, + { + files: ['./src/**/*.{ts,tsx}'], + extends: ['@openshift-assisted/eslint-config', 'plugin:react/jsx-runtime'], + parserOptions: { + tsconfigRootDir: __dirname, + EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, + }, + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: 'react-i18next', + importNames: ['useTranslation'], + message: + 'Import `useTranslation` from `lib/common/hooks/use-translation-wrapper.ts` instead', + }, + { + name: '@openshift-assisted/ui-lib', + message: 'Import from `@openshift-assisted/ui-lib/ocm` instead', + }, + { + name: '@patternfly/react-icons', + message: + 'Import using full path `@patternfly/react-icons/dist/js/icons/` instead', + }, + { + name: '@patternfly/react-tokens', + message: + 'Import using full path `@patternfly/react-tokens/dist/js/` instead', + }, + ], + }, + ], + }, + }, + ], +}; diff --git a/apps/assisted-disconnected-ui/.gitignore b/apps/assisted-disconnected-ui/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/apps/assisted-disconnected-ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/apps/assisted-disconnected-ui/Containerfile b/apps/assisted-disconnected-ui/Containerfile new file mode 100644 index 0000000000..6e3969f964 --- /dev/null +++ b/apps/assisted-disconnected-ui/Containerfile @@ -0,0 +1,24 @@ +FROM registry.access.redhat.com/ubi9/nodejs-18-minimal:latest as ui-build +USER root +RUN microdnf install -y rsync git + +WORKDIR /app +COPY --chown=1001:0 / /app +RUN ls /app +ENV NODE_OPTIONS='--max-old-space-size=8192' +RUN git config --global --add safe.directory /app +RUN npm install -g corepack@0.24.1 +RUN yarn install --immutable && yarn build:all + +FROM registry.access.redhat.com/ubi9/go-toolset:1.21 as proxy-build +WORKDIR /app +COPY apps/assisted-disconnected-ui/proxy /app +USER 0 +RUN go build + +FROM registry.access.redhat.com/ubi9/ubi-micro +COPY --from=ui-build /app/apps/assisted-disconnected-ui/build /app/proxy/dist +COPY --from=proxy-build /app/assisted-disconnected-ui /app/proxy +WORKDIR /app/proxy +EXPOSE 8080 +CMD ./assisted-disconnected-ui diff --git a/apps/assisted-disconnected-ui/README.md b/apps/assisted-disconnected-ui/README.md new file mode 100644 index 0000000000..34f865f7dd --- /dev/null +++ b/apps/assisted-disconnected-ui/README.md @@ -0,0 +1,12 @@ +# The Assisted Installer Disconnected User Interface + +## Setting up a local dev-environment + +- Start [assisted-service](https://github.com/openshift/assisted-service). You can run it + [locally via podman](https://github.com/openshift/assisted-service/tree/master/deploy/podman) + +- Start UI + +```bash + AIUI_APP_API_URL= yarn start:assisted_disconnected_ui +``` diff --git a/apps/assisted-disconnected-ui/index.html b/apps/assisted-disconnected-ui/index.html new file mode 100644 index 0000000000..65f84075dc --- /dev/null +++ b/apps/assisted-disconnected-ui/index.html @@ -0,0 +1,14 @@ + + + + + + + + Assisted Installer + + +
+ + + diff --git a/apps/assisted-disconnected-ui/package.json b/apps/assisted-disconnected-ui/package.json new file mode 100644 index 0000000000..9b8cb6e79e --- /dev/null +++ b/apps/assisted-disconnected-ui/package.json @@ -0,0 +1,64 @@ +{ + "dependencies": { + "@openshift-assisted/ui-lib": "workspace:*", + "@openshift-console/dynamic-plugin-sdk": "0.0.3", + "@patternfly/patternfly": "5.2.0", + "@patternfly/react-code-editor": "5.2.0", + "@patternfly/react-core": "5.2.0", + "@patternfly/react-icons": "5.2.0", + "@patternfly/react-styles": "5.2.0", + "@patternfly/react-table": "5.2.0", + "@patternfly/react-tokens": "5.2.0", + "@reduxjs/toolkit": "^1.9.1", + "@sentry/browser": "^7.119", + "axios": ">=0.22.0 <2.0.0", + "i18next": "^20.4.0", + "i18next-browser-languagedetector": "^6.1.2", + "lodash": "^4", + "monaco-editor": "^0.44.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^11.11.4", + "react-monaco-editor": "^0.55.0", + "react-redux": "^8.0.5", + "react-router-dom": "^5.3.3", + "react-router-dom-v5-compat": "^6.21.2", + "react-tagsinput": "^3.20", + "redux": "^4", + "uuid": "^8.1", + "yup": "^1.4.0" + }, + "description": "A stand-alone web UI for the disconnected environments", + "devDependencies": { + "@tsconfig/vite-react": "^1.0.1", + "@types/react": "17.0.x", + "@vitejs/plugin-react-swc": "^3.0.1", + "concurrently": "^8.2.2", + "nodemon": "^3.0.3", + "vite": "^4.5.6", + "vite-plugin-environment": "^1.1.3" + }, + "overrides": { + "@patternfly/react-core": { + "attr-accept": "2.2.2" + } + }, + "engines": { + "node": ">=14" + }, + "license": "Apache-2.0", + "name": "@openshift-assisted/assisted-disconnected-ui", + "private": true, + "scripts": { + "build": "vite build -c vite.config.ts", + "check_types": "yarn run -T tsc --noEmit", + "clean": "yarn run -T rimraf node_modules build", + "preview": "vite preview -c vite.config.ts", + "serve": "concurrently \"vite serve -c vite.config.ts\" \"cd proxy && nodemon --watch './**/*' --exec 'go run' app.go --signal SIGTERM\"", + "format": "yarn run -T prettier --cache --check . \"!build\"", + "fix-code-style": "yarn lint --fix && yarn format --write", + "lint": "yarn run -T eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache ." + }, + "type": "module", + "version": "1.0.0" +} diff --git a/apps/assisted-disconnected-ui/proxy/app.go b/apps/assisted-disconnected-ui/proxy/app.go new file mode 100644 index 0000000000..cd6dac219e --- /dev/null +++ b/apps/assisted-disconnected-ui/proxy/app.go @@ -0,0 +1,61 @@ +package main + +import ( + "crypto/tls" + "net/http" + "time" + + "github.com/gorilla/mux" + + "github.com/openshift-assisted/assisted-disconnected-ui/bridge" + "github.com/openshift-assisted/assisted-disconnected-ui/config" + "github.com/openshift-assisted/assisted-disconnected-ui/log" + "github.com/openshift-assisted/assisted-disconnected-ui/server" +) + +func main() { + tlsConfig, err := bridge.GetTlsConfig() + if err != nil { + panic(err) + } + + log := log.InitLogs() + router := mux.NewRouter() + + apiHandler := bridge.NewAssistedAPIHandler(tlsConfig) + router.PathPrefix("/api/{forward:.*}").Handler(apiHandler) + + spa := server.SpaHandler{} + router.PathPrefix("/").Handler(server.GzipHandler(spa)) + + var serverTlsconfig *tls.Config + + if config.TlsKeyPath != "" && config.TlsCertPath != "" { + cert, err := tls.LoadX509KeyPair(config.TlsCertPath, config.TlsKeyPath) + if err != nil { + panic(err) + } + serverTlsconfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + + } + + srv := &http.Server{ + Handler: router, + Addr: config.BridgePort, + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Info("Proxy running at", config.BridgePort) + + if serverTlsconfig != nil { + srv.TLSConfig = serverTlsconfig + log.Info("Running as HTTPS") + log.Fatal(srv.ListenAndServeTLS("", "")) + } else { + log.Fatal(srv.ListenAndServe()) + } + +} diff --git a/apps/assisted-disconnected-ui/proxy/bridge/common.go b/apps/assisted-disconnected-ui/proxy/bridge/common.go new file mode 100644 index 0000000000..f69b858c2e --- /dev/null +++ b/apps/assisted-disconnected-ui/proxy/bridge/common.go @@ -0,0 +1,35 @@ +package bridge + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "os" + + "github.com/openshift-assisted/assisted-disconnected-ui/config" + log "github.com/sirupsen/logrus" +) + +func GetTlsConfig() (*tls.Config, error) { + tlsConfig := &tls.Config{} + + if config.ApiInsecure == "true" { + log.Warn("Using InsecureSkipVerify for API communication") + tlsConfig.InsecureSkipVerify = true + } + + _, err := os.Stat("../certs/ca.crt") + if errors.Is(err, os.ErrNotExist) { + return tlsConfig, nil + } + caCert, err := os.ReadFile("../certs/ca.crt") + if err != nil { + return nil, err + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + tlsConfig.RootCAs = caCertPool + return tlsConfig, nil +} diff --git a/apps/assisted-disconnected-ui/proxy/bridge/handler.go b/apps/assisted-disconnected-ui/proxy/bridge/handler.go new file mode 100644 index 0000000000..405a11f130 --- /dev/null +++ b/apps/assisted-disconnected-ui/proxy/bridge/handler.go @@ -0,0 +1,57 @@ +package bridge + +import ( + "crypto/tls" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/gorilla/mux" + + "github.com/openshift-assisted/assisted-disconnected-ui/config" +) + +type handler struct { + target *url.URL + proxy *httputil.ReverseProxy +} + +func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r.URL.Host = h.target.Host + r.URL.Scheme = h.target.Scheme + r.Header.Set("X-Forwarded-Host", r.Header.Get("Host")) + r.Host = h.target.Host + r.URL.Path = mux.Vars(r)["forward"] + h.proxy.ServeHTTP(w, r) +} + +func createReverseProxy(apiURL string) (*url.URL, *httputil.ReverseProxy) { + target, err := url.Parse(apiURL) + if err != nil { + panic(err) + } + proxy := httputil.NewSingleHostReverseProxy(target) + proxy.ModifyResponse = func(r *http.Response) error { + filterHeaders := []string{ + "Access-Control-Allow-Headers", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Origin", + "Access-Control-Expose-Headers", + } + for _, h := range filterHeaders { + r.Header.Del(h) + } + return nil + } + return target, proxy +} + +func NewAssistedAPIHandler(tlsConfig *tls.Config) handler { + target, proxy := createReverseProxy(config.AssistedApiUrl) + + proxy.Transport = &http.Transport{ + TLSClientConfig: tlsConfig, + } + + return handler{target: target, proxy: proxy} +} diff --git a/apps/assisted-disconnected-ui/proxy/config/config.go b/apps/assisted-disconnected-ui/proxy/config/config.go new file mode 100644 index 0000000000..ad8c075523 --- /dev/null +++ b/apps/assisted-disconnected-ui/proxy/config/config.go @@ -0,0 +1,27 @@ +package config + +import ( + "os" + "strings" +) + +var ( + BridgePort = ":" + getEnvVar("BRIDGE_PORT", "3001") + AssistedApiUrl = getEnvUrlVar("AIUI_APP_API_URL", "") + ApiInsecure = getEnvVar("API_INSECURE_SKIP_VERIFY", "false") + TlsKeyPath = getEnvVar("TLS_KEY", "") + TlsCertPath = getEnvVar("TLS_CERT", "") +) + +func getEnvUrlVar(key string, defaultValue string) string { + urlValue := getEnvVar(key, defaultValue) + return strings.TrimSuffix(urlValue, "/") +} + +func getEnvVar(key string, defaultValue string) string { + val, ok := os.LookupEnv(key) + if !ok { + return defaultValue + } + return val +} diff --git a/apps/assisted-disconnected-ui/proxy/go.mod b/apps/assisted-disconnected-ui/proxy/go.mod new file mode 100644 index 0000000000..dcda9fd5c1 --- /dev/null +++ b/apps/assisted-disconnected-ui/proxy/go.mod @@ -0,0 +1,13 @@ +module github.com/openshift-assisted/assisted-disconnected-ui + +go 1.21 + +require ( + github.com/gorilla/mux v1.8.1 + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/stretchr/testify v1.9.0 // indirect + golang.org/x/sys v0.22.0 // indirect +) diff --git a/apps/assisted-disconnected-ui/proxy/go.sum b/apps/assisted-disconnected-ui/proxy/go.sum new file mode 100644 index 0000000000..117fecf073 --- /dev/null +++ b/apps/assisted-disconnected-ui/proxy/go.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/apps/assisted-disconnected-ui/proxy/log/log.go b/apps/assisted-disconnected-ui/proxy/log/log.go new file mode 100644 index 0000000000..75f262ce96 --- /dev/null +++ b/apps/assisted-disconnected-ui/proxy/log/log.go @@ -0,0 +1,13 @@ +package log + +import ( + "github.com/sirupsen/logrus" +) + +func InitLogs() *logrus.Logger { + log := logrus.New() + + log.SetReportCaller(true) + + return log +} diff --git a/apps/assisted-disconnected-ui/proxy/server/compression.go b/apps/assisted-disconnected-ui/proxy/server/compression.go new file mode 100644 index 0000000000..a3ea0585c1 --- /dev/null +++ b/apps/assisted-disconnected-ui/proxy/server/compression.go @@ -0,0 +1,39 @@ +package server + +import ( + "compress/gzip" + "io" + "net/http" + "strings" +) + +type gzipResponseWriter struct { + io.Writer + http.ResponseWriter + sniffDone bool +} + +func (w *gzipResponseWriter) Write(b []byte) (int, error) { + if !w.sniffDone { + if w.Header().Get("Content-Type") == "" { + w.Header().Set("Content-Type", http.DetectContentType(b)) + } + w.sniffDone = true + } + return w.Writer.Write(b) +} + +// gzipHandler wraps a http.Handler to support transparent gzip encoding. +func GzipHandler(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Vary", "Accept-Encoding") + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + h.ServeHTTP(w, r) + return + } + w.Header().Set("Content-Encoding", "gzip") + gz := gzip.NewWriter(w) + defer gz.Close() + h.ServeHTTP(&gzipResponseWriter{Writer: gz, ResponseWriter: w}, r) + } +} diff --git a/apps/assisted-disconnected-ui/proxy/server/server.go b/apps/assisted-disconnected-ui/proxy/server/server.go new file mode 100644 index 0000000000..fb672b2f81 --- /dev/null +++ b/apps/assisted-disconnected-ui/proxy/server/server.go @@ -0,0 +1,40 @@ +package server + +import ( + "bytes" + "net/http" + "os" + "path/filepath" + "time" +) + +type SpaHandler struct{} + +func serveIndexPage(w http.ResponseWriter, r *http.Request) { + content, err := os.ReadFile("./dist/index.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(content)) + +} + +func (h SpaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := filepath.Join("./dist", r.URL.Path) + fi, err := os.Stat(path) + if os.IsNotExist(err) || fi.IsDir() || path == "index.html" { + serveIndexPage(w, r) + return + } + + if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static file + http.FileServer(http.Dir("./dist")).ServeHTTP(w, r) +} diff --git a/apps/assisted-disconnected-ui/public/favicon.ico b/apps/assisted-disconnected-ui/public/favicon.ico new file mode 100644 index 0000000000..7206fc4959 Binary files /dev/null and b/apps/assisted-disconnected-ui/public/favicon.ico differ diff --git a/apps/assisted-disconnected-ui/public/logo.svg b/apps/assisted-disconnected-ui/public/logo.svg new file mode 100644 index 0000000000..b7a88d40a5 --- /dev/null +++ b/apps/assisted-disconnected-ui/public/logo.svg @@ -0,0 +1 @@ +Logo-Red_Hat-OpenShift_Container_Platform-B-Black-RGB \ No newline at end of file diff --git a/apps/assisted-disconnected-ui/src/components/App.tsx b/apps/assisted-disconnected-ui/src/components/App.tsx new file mode 100755 index 0000000000..02e3425452 --- /dev/null +++ b/apps/assisted-disconnected-ui/src/components/App.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { BrowserRouter, Routes, Route } from 'react-router-dom-v5-compat'; +import { Brand, Masthead, MastheadBrand, MastheadMain, Page } from '@patternfly/react-core'; +import '../i18n'; +import Wizard from './Wizard'; +import { Provider } from 'react-redux'; +import { Store } from '@openshift-assisted/ui-lib/ocm'; +import EditWizard from './EditWizard'; + +export const App: React.FC = () => { + const header = ( + + + + + + + + + + ); + + return ( + + + + + } /> + } /> + + + + + ); +}; diff --git a/apps/assisted-disconnected-ui/src/components/EditWizard.tsx b/apps/assisted-disconnected-ui/src/components/EditWizard.tsx new file mode 100644 index 0000000000..b19bc05af3 --- /dev/null +++ b/apps/assisted-disconnected-ui/src/components/EditWizard.tsx @@ -0,0 +1,82 @@ +import { + AlertsContextProvider, + CpuArchitecture, + ErrorState, + ResourceUIState, +} from '@openshift-assisted/ui-lib/common'; +import { + ClusterLoading, + ClusterWizardContextProvider, + useClusterPolling, + ClusterWizard, + useInfraEnv, + ModalDialogsContextProvider, + ClusterDefaultConfigurationProvider, + ClusterUiError, + OpenshiftVersionsContextProvider, + NewFeatureSupportLevelProvider, +} from '@openshift-assisted/ui-lib/ocm'; +import { PageSection, PageSectionVariants } from '@patternfly/react-core'; +import { useParams } from 'react-router-dom-v5-compat'; + +const EditWizard = () => { + const { clusterId } = useParams<{ clusterId: string }>() as { clusterId: string }; + const { cluster, uiState, errorDetail } = useClusterPolling(clusterId); + const pullSecret = ''; + const { + infraEnv, + isLoading: infraEnvLoading, + error: infraEnvError, + updateInfraEnv, + } = useInfraEnv( + clusterId, + cluster?.cpuArchitecture + ? (cluster.cpuArchitecture as CpuArchitecture) + : CpuArchitecture.USE_DAY1_ARCHITECTURE, + cluster?.name, + pullSecret, + cluster?.openshiftVersion, + ); + + if (uiState === ResourceUIState.LOADING || infraEnvLoading || !cluster || !infraEnv) { + return ; + } + + if (uiState === ResourceUIState.POLLING_ERROR || infraEnvError) { + return ( + + + + ); + } + + return ( + + + } + errorUI={} + > + + }> + + + + + + + + + + + ); +}; + +export default EditWizard; diff --git a/apps/assisted-disconnected-ui/src/components/Wizard.tsx b/apps/assisted-disconnected-ui/src/components/Wizard.tsx new file mode 100644 index 0000000000..d4fcd57965 --- /dev/null +++ b/apps/assisted-disconnected-ui/src/components/Wizard.tsx @@ -0,0 +1,37 @@ +import { AlertsContextProvider } from '@openshift-assisted/ui-lib/common'; +import { + ModalDialogsContextProvider, + OpenshiftVersionsContextProvider, + ClusterUiError, + ClusterDefaultConfigurationProvider, + ClusterLoading, + ClusterWizardContextProvider, + NewClusterWizard, + NewFeatureSupportLevelProvider, +} from '@openshift-assisted/ui-lib/ocm'; +import { PageSection, PageSectionVariants } from '@patternfly/react-core'; + +const Wizard = () => { + return ( + + + } + errorUI={} + > + + }> + + + + + + + + + + + ); +}; + +export default Wizard; diff --git a/apps/assisted-disconnected-ui/src/i18n.ts b/apps/assisted-disconnected-ui/src/i18n.ts new file mode 100644 index 0000000000..c5000fdf43 --- /dev/null +++ b/apps/assisted-disconnected-ui/src/i18n.ts @@ -0,0 +1,59 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import translation from '@openshift-assisted/locales/en/translation.json'; + +const dateTimeFormatter = new Intl.DateTimeFormat('default', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + year: 'numeric', +}); + +void i18n.use(initReactI18next).init({ + lng: 'en', + resources: { + en: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + translation, + }, + }, + supportedLngs: ['en'], + fallbackLng: 'en', + load: 'languageOnly', + detection: { caches: [] }, + defaultNS: 'translation', + nsSeparator: '~', + keySeparator: false, + debug: true, + interpolation: { + format(value, format, lng) { + let output = value as unknown; + if (format === 'number' && (typeof value === 'number' || typeof value === 'bigint')) { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat#Browser_compatibility + output = new Intl.NumberFormat(lng).format(value); + } + + if (value instanceof Date) { + output = dateTimeFormatter.format(value); + } + + return String(output); + }, + escapeValue: false, // not needed for react as it escapes by default + }, + react: { + useSuspense: true, + transSupportBasicHtmlNodes: true, // allow
and simple html elements in translations + }, + missingKeyHandler(lng, ns, key) { + if (lng instanceof Array) { + for (const language of lng) { + // eslint-disable-next-line no-console + console.warn(`Missing i18n key '${key}' in namespace '${ns}' and language '${language}.'`); + } + } + }, +}); + +export default i18n; diff --git a/apps/assisted-disconnected-ui/src/main.tsx b/apps/assisted-disconnected-ui/src/main.tsx new file mode 100755 index 0000000000..96e9797e0d --- /dev/null +++ b/apps/assisted-disconnected-ui/src/main.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './components/App'; + +const rootElement = document.getElementById('root'); +if (rootElement) { + const root = ReactDOM.createRoot(rootElement); + root.render( + + + , + ); +} diff --git a/apps/assisted-disconnected-ui/src/styles.css b/apps/assisted-disconnected-ui/src/styles.css new file mode 100644 index 0000000000..a9648eb112 --- /dev/null +++ b/apps/assisted-disconnected-ui/src/styles.css @@ -0,0 +1,2 @@ +@import '@patternfly/patternfly/patternfly.css'; +@import '@patternfly/patternfly/patternfly-addons.css'; diff --git a/apps/assisted-disconnected-ui/tsconfig.json b/apps/assisted-disconnected-ui/tsconfig.json new file mode 100644 index 0000000000..dcea18059d --- /dev/null +++ b/apps/assisted-disconnected-ui/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/vite-react/tsconfig.json", + "include": ["src", "vite.config.ts"], + "references": [ + { + "path": "../../libs/ui-lib" + } + ] +} diff --git a/apps/assisted-disconnected-ui/vite.config.ts b/apps/assisted-disconnected-ui/vite.config.ts new file mode 100644 index 0000000000..ecb4cf684d --- /dev/null +++ b/apps/assisted-disconnected-ui/vite.config.ts @@ -0,0 +1,45 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; +import EnvironmentPlugin from 'vite-plugin-environment'; +import 'zx/globals'; + +export const getDefaultValuesForEnvironmentVariables = async () => { + $.verbose = false; + const commitSignature = (await $`git rev-parse --short HEAD`).toString().trim(); + + return { + AIUI_APP_IMAGE_REPO: 'quay.io/edge-infrastructure/assisted-disconnected-ui', + AIUI_APP_GIT_SHA: commitSignature, + AIUI_APP_VERSION: `latest+sha.${commitSignature}`, + }; +}; + +export default defineConfig(async () => { + const envVarsPrefix = 'AIUI_'; + const defaultValues = await getDefaultValuesForEnvironmentVariables(); + + return { + build: { + emptyOutDir: true, + outDir: 'build', + sourcemap: true, + }, + resolve: { + conditions: ['source'], + }, + plugins: [ + EnvironmentPlugin(defaultValues, { + prefix: envVarsPrefix, + defineOn: 'process.env', + }), + react(), + ], + server: { + proxy: { + '/api': { + target: 'http://localhost:3001/api', + }, + }, + }, + }; +}); diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/index.ts b/libs/ui-lib/lib/ocm/components/clusterWizard/index.ts index 714829c504..1dafffed58 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/index.ts +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/index.ts @@ -1,3 +1,5 @@ export { default as ClusterDetailsForm } from './ClusterDetailsForm'; export { default as NewClusterWizard } from './NewClusterWizard'; +export { default as ClusterWizard } from './ClusterWizard'; export { default as ClusterWizardContextProvider } from './ClusterWizardContextProvider'; +export { OpenshiftVersionsContextProvider } from './OpenshiftVersionsContext'; diff --git a/libs/ui-lib/lib/ocm/components/clusters/index.ts b/libs/ui-lib/lib/ocm/components/clusters/index.ts index fab3088ed7..9d9e882b2e 100644 --- a/libs/ui-lib/lib/ocm/components/clusters/index.ts +++ b/libs/ui-lib/lib/ocm/components/clusters/index.ts @@ -1,6 +1,9 @@ export { default as Clusters } from './Clusters'; +export { default as ClusterLoading } from './ClusterLoading'; export { default as ClusterToolbar } from './ClusterToolbar'; +export { ClusterUiError } from './ClusterPageErrors'; export { default as ClusterStatus, ClusterStatusIcon } from './ClusterStatus'; export * from './NewClusterPage'; export * from './ClusterPage'; +export { useClusterPolling } from './clusterPolling'; diff --git a/libs/ui-lib/lib/ocm/components/index.ts b/libs/ui-lib/lib/ocm/components/index.ts index 7de09186fc..1f024501a3 100644 --- a/libs/ui-lib/lib/ocm/components/index.ts +++ b/libs/ui-lib/lib/ocm/components/index.ts @@ -6,3 +6,5 @@ export * from './AddHosts'; export * from './clusterWizard'; export * from './HostsClusterDetailTab'; export * from './featureSupportLevels'; +export * from './clusterConfiguration/ClusterDefaultConfigurationContext'; +export * from './hosts/ModalDialogsContext'; diff --git a/libs/ui-lib/lib/ocm/hooks/index.ts b/libs/ui-lib/lib/ocm/hooks/index.ts index 51bdb62cbd..1fd76841ff 100644 --- a/libs/ui-lib/lib/ocm/hooks/index.ts +++ b/libs/ui-lib/lib/ocm/hooks/index.ts @@ -4,3 +4,4 @@ export { default as useInfraEnvId } from './useInfraEnvId'; export { default as usePullSecret } from './usePullSecret'; export { default as useClusterPreflightRequirements } from './useClusterPreflightRequirements'; export { default as useUISettings } from './useUISettings'; +export { default as useInfraEnv } from './useInfraEnv'; diff --git a/libs/ui-lib/lib/ocm/hooks/useInfraEnv.ts b/libs/ui-lib/lib/ocm/hooks/useInfraEnv.ts index 4ee0eca127..49ecaa7f29 100644 --- a/libs/ui-lib/lib/ocm/hooks/useInfraEnv.ts +++ b/libs/ui-lib/lib/ocm/hooks/useInfraEnv.ts @@ -1,5 +1,5 @@ import React from 'react'; -import { useInfraEnvId } from '.'; +import useInfraEnvId from './useInfraEnvId'; import { CpuArchitecture } from '../../common'; import { getErrorMessage } from '../../common/utils'; import { InfraEnvsAPI } from '../services/apis'; diff --git a/libs/ui-lib/lib/ocm/index.ts b/libs/ui-lib/lib/ocm/index.ts index ba876c73b7..5f89b9e8fc 100644 --- a/libs/ui-lib/lib/ocm/index.ts +++ b/libs/ui-lib/lib/ocm/index.ts @@ -6,6 +6,7 @@ export * as Services from './services'; // without namespace export * from './components'; export * from './services'; +export * from './hooks'; // re-export selected from common export * as Features from '../common/features'; diff --git a/package.json b/package.json index 66d77b0de8..e8b6625a10 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "test:run": "yarn workspace @openshift-assisted/ui-lib-tests run cy:run", "test:unit": "yarn workspaces foreach -v run test", "start:assisted_ui": "yarn workspace @openshift-assisted/assisted-ui serve", + "start:assisted_disconnected_ui": "yarn workspace @openshift-assisted/assisted-disconnected-ui serve", "start:watch_mode": "yarn build:all && yarn run -T toolbox watch --dir=libs/ui-lib --dir=libs/types --dir=libs/locales 'yarn _build:ui-lib' 'yarn _yalc:push'", "start:vitest-ui": "vitest --ui" }, diff --git a/yarn.lock b/yarn.lock index fc46d4e076..112c946da3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -744,6 +744,47 @@ __metadata: languageName: node linkType: hard +"@openshift-assisted/assisted-disconnected-ui@workspace:apps/assisted-disconnected-ui": + version: 0.0.0-use.local + resolution: "@openshift-assisted/assisted-disconnected-ui@workspace:apps/assisted-disconnected-ui" + dependencies: + "@openshift-assisted/ui-lib": "workspace:*" + "@openshift-console/dynamic-plugin-sdk": 0.0.3 + "@patternfly/patternfly": 5.2.0 + "@patternfly/react-code-editor": 5.2.0 + "@patternfly/react-core": 5.2.0 + "@patternfly/react-icons": 5.2.0 + "@patternfly/react-styles": 5.2.0 + "@patternfly/react-table": 5.2.0 + "@patternfly/react-tokens": 5.2.0 + "@reduxjs/toolkit": ^1.9.1 + "@sentry/browser": ^7.119 + "@tsconfig/vite-react": ^1.0.1 + "@types/react": 17.0.x + "@vitejs/plugin-react-swc": ^3.0.1 + axios: ">=0.22.0 <2.0.0" + concurrently: ^8.2.2 + i18next: ^20.4.0 + i18next-browser-languagedetector: ^6.1.2 + lodash: ^4 + monaco-editor: ^0.44.0 + nodemon: ^3.0.3 + react: ^18.2.0 + react-dom: ^18.2.0 + react-i18next: ^11.11.4 + react-monaco-editor: ^0.55.0 + react-redux: ^8.0.5 + react-router-dom: ^5.3.3 + react-router-dom-v5-compat: ^6.21.2 + react-tagsinput: ^3.20 + redux: ^4 + uuid: ^8.1 + vite: ^4.5.6 + vite-plugin-environment: ^1.1.3 + yup: ^1.4.0 + languageName: unknown + linkType: soft + "@openshift-assisted/assisted-ui@workspace:apps/assisted-ui": version: 0.0.0-use.local resolution: "@openshift-assisted/assisted-ui@workspace:apps/assisted-ui" @@ -2969,7 +3010,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": +"chokidar@npm:^3.4.2, chokidar@npm:^3.5.2, chokidar@npm:^3.5.3": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -3295,6 +3336,26 @@ __metadata: languageName: node linkType: hard +"concurrently@npm:^8.2.2": + version: 8.2.2 + resolution: "concurrently@npm:8.2.2" + dependencies: + chalk: ^4.1.2 + date-fns: ^2.30.0 + lodash: ^4.17.21 + rxjs: ^7.8.1 + shell-quote: ^1.8.1 + spawn-command: 0.0.2 + supports-color: ^8.1.1 + tree-kill: ^1.2.2 + yargs: ^17.7.2 + bin: + conc: dist/bin/concurrently.js + concurrently: dist/bin/concurrently.js + checksum: 8ac774df06869773438f1bf91025180c52d5b53139bc86cf47659136c0d97461d0579c515d848d1e945d4e3e0cafe646b2ea18af8d74259b46abddcfe39b2c6c + languageName: node + linkType: hard + "confbox@npm:^0.1.7": version: 0.1.7 resolution: "confbox@npm:0.1.7" @@ -3519,7 +3580,7 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^2.16.1": +"date-fns@npm:^2.16.1, date-fns@npm:^2.30.0": version: 2.30.0 resolution: "date-fns@npm:2.30.0" dependencies: @@ -3572,6 +3633,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4": + version: 4.4.0 + resolution: "debug@npm:4.4.0" + dependencies: + ms: ^2.1.3 + peerDependenciesMeta: + supports-color: + optional: true + checksum: fb42df878dd0e22816fc56e1fdca9da73caa85212fbe40c868b1295a6878f9101ae684f4eeef516c13acfc700f5ea07f1136954f43d4cd2d477a811144136479 + languageName: node + linkType: hard + "deep-eql@npm:^4.1.3": version: 4.1.3 resolution: "deep-eql@npm:4.1.3" @@ -5618,6 +5691,13 @@ __metadata: languageName: node linkType: hard +"ignore-by-default@npm:^1.0.1": + version: 1.0.1 + resolution: "ignore-by-default@npm:1.0.1" + checksum: 441509147b3615e0365e407a3c18e189f78c07af08564176c680be1fabc94b6c789cad1342ad887175d4ecd5225de86f73d376cec8e06b42fd9b429505ffcf8a + languageName: node + linkType: hard + "ignore@npm:^5.2.0, ignore@npm:^5.2.4": version: 5.3.1 resolution: "ignore@npm:5.3.1" @@ -6917,7 +6997,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.1.1": +"ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -7057,6 +7137,26 @@ __metadata: languageName: node linkType: hard +"nodemon@npm:^3.0.3": + version: 3.1.9 + resolution: "nodemon@npm:3.1.9" + dependencies: + chokidar: ^3.5.2 + debug: ^4 + ignore-by-default: ^1.0.1 + minimatch: ^3.1.2 + pstree.remy: ^1.1.8 + semver: ^7.5.3 + simple-update-notifier: ^2.0.0 + supports-color: ^5.5.0 + touch: ^3.1.0 + undefsafe: ^2.0.5 + bin: + nodemon: bin/nodemon.js + checksum: d045065dea08904f1356d18132538e71a61df12cb4e2852730310492943676d4789bedb28c343a5d85d5e07558bf47b73f000a8017409f0b7d522a3c1c42b2e5 + languageName: node + linkType: hard + "nopt@npm:^7.0.0": version: 7.2.0 resolution: "nopt@npm:7.2.0" @@ -7719,6 +7819,13 @@ __metadata: languageName: node linkType: hard +"pstree.remy@npm:^1.1.8": + version: 1.1.8 + resolution: "pstree.remy@npm:1.1.8" + checksum: 5cb53698d6bb34dfb278c8a26957964aecfff3e161af5fbf7cee00bbe9d8547c7aced4bd9cb193bce15fb56e9e4220fc02a5bf9c14345ffb13a36b858701ec2d + languageName: node + linkType: hard + "pump@npm:^2.0.0": version: 2.0.1 resolution: "pump@npm:2.0.1" @@ -8655,6 +8762,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:^1.8.1": + version: 1.8.2 + resolution: "shell-quote@npm:1.8.2" + checksum: 1e97b62ced1c4c5135015978ebf273bed1f425a68cf84163e83fbb0f34b3ff9471e656720dab2b7cbb4ae0f58998e686d17d166c28dfb3662acd009e8bd7faed + languageName: node + linkType: hard + "side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": version: 1.0.6 resolution: "side-channel@npm:1.0.6" @@ -8688,6 +8802,15 @@ __metadata: languageName: node linkType: hard +"simple-update-notifier@npm:^2.0.0": + version: 2.0.0 + resolution: "simple-update-notifier@npm:2.0.0" + dependencies: + semver: ^7.5.3 + checksum: 9ba00d38ce6a29682f64a46213834e4eb01634c2f52c813a9a7b8873ca49cdbb703696f3290f3b27dc067de6d9418b0b84bef22c3eb074acf352529b2d6c27fd + languageName: node + linkType: hard + "sirv@npm:^2.0.3": version: 2.0.4 resolution: "sirv@npm:2.0.4" @@ -8780,7 +8903,7 @@ __metadata: languageName: node linkType: hard -"spawn-command@npm:^0.0.2-1": +"spawn-command@npm:0.0.2, spawn-command@npm:^0.0.2-1": version: 0.0.2 resolution: "spawn-command@npm:0.0.2" checksum: e35c5d28177b4d461d33c88cc11f6f3a5079e2b132c11e1746453bbb7a0c0b8a634f07541a2a234fa4758239d88203b758def509161b651e81958894c0b4b64b @@ -9078,7 +9201,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^5.3.0": +"supports-color@npm:^5.3.0, supports-color@npm:^5.5.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" dependencies: @@ -9331,6 +9454,15 @@ __metadata: languageName: node linkType: hard +"touch@npm:^3.1.0": + version: 3.1.1 + resolution: "touch@npm:3.1.1" + bin: + nodetouch: bin/nodetouch.js + checksum: fb8c54207500eb760b6b9d77b9c5626cc027c9ad44431eed4268845f00f8c6bbfc95ce7e9da8e487f020aa921982a8bc5d8e909d0606e82686bd0a08a8e0539b + languageName: node + linkType: hard + "tough-cookie@npm:^4.1.3": version: 4.1.3 resolution: "tough-cookie@npm:4.1.3" @@ -9583,6 +9715,13 @@ __metadata: languageName: node linkType: hard +"undefsafe@npm:^2.0.5": + version: 2.0.5 + resolution: "undefsafe@npm:2.0.5" + checksum: f42ab3b5770fedd4ada175fc1b2eb775b78f609156f7c389106aafd231bfc210813ee49f54483d7191d7b76e483bc7f537b5d92d19ded27156baf57592eb02cc + languageName: node + linkType: hard + "underscore.string@npm:~3.3.4": version: 3.3.6 resolution: "underscore.string@npm:3.3.6"