Skip to content

feat(web): initial work to setup root handler in the web component #2192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 29, 2025
Merged
1 change: 0 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ func (a *API) RegisterRoutes(router *mux.Router) {
router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler)

router.HandleFunc("/auth/login", a.postAuthLogin).Methods("POST")
router.HandleFunc("/branding", a.getBranding).Methods("GET")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer need the branding routes.


authenticatedRouter := router.PathPrefix("/").Subrouter()
authenticatedRouter.Use(a.authMiddleware)
Expand Down
21 changes: 0 additions & 21 deletions api/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,8 @@ package api

import (
"net/http"

"github.com/replicatedhq/embedded-cluster/api/types"
)

type getBrandingResponse struct {
Branding types.Branding `json:"branding"`
}

func (a *API) getBranding(w http.ResponseWriter, r *http.Request) {
branding, err := a.consoleController.GetBranding()
if err != nil {
a.logError(r, err, "failed to get branding")
a.jsonError(w, r, err)
return
}

response := getBrandingResponse{
Branding: branding,
}

a.json(w, r, http.StatusOK, response)
}

type getListAvailableNetworkInterfacesResponse struct {
NetworkInterfaces []string `json:"networkInterfaces"`
}
Expand Down
17 changes: 0 additions & 17 deletions api/controllers/console/controller.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package console

import (
"fmt"

"github.com/replicatedhq/embedded-cluster/api/pkg/utils"
"github.com/replicatedhq/embedded-cluster/api/types"
"github.com/replicatedhq/embedded-cluster/pkg/release"
)

type Controller interface {
GetBranding() (types.Branding, error)
ListAvailableNetworkInterfaces() ([]string, error)
}

Expand Down Expand Up @@ -41,18 +36,6 @@ func NewConsoleController(opts ...ConsoleControllerOption) (*ConsoleController,
return controller, nil
}

func (c *ConsoleController) GetBranding() (types.Branding, error) {
app := release.GetApplication()
if app == nil {
return types.Branding{}, fmt.Errorf("application not found")
}

return types.Branding{
AppTitle: app.Spec.Title,
AppIcon: app.Spec.Icon,
}, nil
}

func (c *ConsoleController) ListAvailableNetworkInterfaces() ([]string, error) {
return c.NetUtils.ListValidNetworkInterfaces()
}
6 changes: 0 additions & 6 deletions api/types/console.go

This file was deleted.

92 changes: 0 additions & 92 deletions api/types/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -224,94 +223,3 @@ func TestAPIError_ErrorOrNil(t *testing.T) {
})
}
}

func TestAPIError_JSON(t *testing.T) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some mismatch between PRs that have been merged, this fixes some go vet issues:

tests := []struct {
name string
apiErr *APIError
wantCode int
wantJSON map[string]any
}{
{
name: "simple error",
apiErr: &APIError{
StatusCode: http.StatusInternalServerError,
Message: "invalid request",
},
wantCode: http.StatusInternalServerError,
wantJSON: map[string]any{
"status_code": float64(http.StatusInternalServerError),
"message": "invalid request",
},
},
{
name: "field error",
apiErr: &APIError{
StatusCode: http.StatusBadRequest,
Message: "validation error",
Field: "username",
},
wantCode: http.StatusBadRequest,
wantJSON: map[string]any{
"status_code": float64(http.StatusBadRequest),
"message": "validation error",
"field": "username",
},
},
{
name: "error with nested errors",
apiErr: &APIError{
StatusCode: http.StatusBadRequest,
Message: "multiple validation errors",
Errors: []*APIError{
{
Message: "field1 is required",
Field: "field1",
},
{
Message: "field2 must be a number",
Field: "field2",
},
},
},
wantCode: http.StatusBadRequest,
wantJSON: map[string]any{
"status_code": float64(http.StatusBadRequest),
"message": "multiple validation errors",
"errors": []any{
map[string]any{
"message": "field1 is required",
"field": "field1",
},
map[string]any{
"message": "field2 must be a number",
"field": "field2",
},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a mock HTTP response recorder
rec := httptest.NewRecorder()

// Call the JSON method
tt.apiErr.JSON(rec)

// Check status code
assert.Equal(t, tt.wantCode, rec.Code, "Status code should match")

// Check content type header
contentType := rec.Header().Get("Content-Type")
assert.Equal(t, "application/json", contentType, "Content-Type header should be application/json")

// Parse and check the JSON response
var gotJSON map[string]any
err := json.Unmarshal(rec.Body.Bytes(), &gotJSON)
assert.NoError(t, err, "Should be able to parse the JSON response")
assert.Equal(t, tt.wantJSON, gotJSON, "JSON response should match expected structure")
})
}
}
25 changes: 17 additions & 8 deletions cmd/installer/cli/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"net"
"net/http"
Expand Down Expand Up @@ -97,6 +98,9 @@ type InstallCmdFlags struct {
tlsKeyBytes []byte
}

// webAssetsFS is the filesystem to be used by the web component. Defaults to nil allowing the web server to use the default assets embedded in the binary. Useful for testing.
var webAssetsFS fs.FS = nil
Comment on lines +101 to +102
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not particularly happy about this, unsure if we should keep this somewhere else, open to ideas.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than that README.md hack could you have a stub index.html?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main issue with that is when building the web assets, index.html will change and it's not gitignored so you'll end up with changes that shouldn't be staged for commit.


// InstallCmd returns a cobra command for installing the embedded cluster.
func InstallCmd(ctx context.Context, name string) *cobra.Command {
var flags InstallCmdFlags
Expand Down Expand Up @@ -455,16 +459,21 @@ func runInstallAPI(ctx context.Context, listener net.Listener, cert tls.Certific
if err != nil {
return fmt.Errorf("new api: %w", err)
}
app := release.GetApplication()
if app == nil {
return fmt.Errorf("application not found")
}

api.RegisterRoutes(router.PathPrefix("/api").Subrouter())

var webFs http.Handler
if os.Getenv("EC_DEV_ENV") == "true" {
webFs = http.FileServer(http.FS(os.DirFS("./web/dist")))
} else {
webFs = http.FileServer(http.FS(web.Fs()))
webServer, err := web.New(web.InitialState{
Title: app.Spec.Title,
Icon: app.Spec.Icon,
}, web.WithLogger(logger), web.WithAssetsFS(webAssetsFS))
if err != nil {
return fmt.Errorf("new web server: %w", err)
}
router.PathPrefix("/").Methods("GET").Handler(webFs)

api.RegisterRoutes(router.PathPrefix("/api").Subrouter())
webServer.RegisterRoutes(router.PathPrefix("/").Subrouter())

server := &http.Server{
// ErrorLog outputs TLS errors and warnings to the console, we want to make sure we use the same logrus logger for them
Expand Down
26 changes: 25 additions & 1 deletion cmd/installer/cli/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"path/filepath"
"testing"
"testing/fstest"
"time"

"github.com/replicatedhq/embedded-cluster/api"
Expand Down Expand Up @@ -560,6 +561,29 @@ func Test_runInstallAPI(t *testing.T) {
certPool := x509.NewCertPool()
certPool.AddCert(cert.Leaf)

// We need a release object to pass over to the Web component.
dataMap := map[string][]byte{
"kots-app.yaml": []byte(`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably include an icon and a title

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can, albeit it's not really being used right now. Unless we also want to run some requests within this test against the web components?

apiVersion: kots.io/v1beta1
kind: Application
`),
}
err = release.SetReleaseDataForTests(dataMap)
require.NoError(t, err)

t.Cleanup(func() {
release.SetReleaseDataForTests(nil)
})

// Mock the web assets filesystem so that we don't need to embed the web assets.
webAssetsFS = fstest.MapFS{
"index.html": &fstest.MapFile{
Data: []byte(""),
Mode: 0644,
},
}
defer func() { webAssetsFS = nil }()

go func() {
err := runInstallAPI(ctx, listener, cert, logger, "password", nil)
t.Logf("Install API exited with error: %v", err)
Expand All @@ -581,7 +605,7 @@ func Test_runInstallAPI(t *testing.T) {
},
}
resp, err := httpClient.Get(url)
assert.NoError(t, err)
require.NoError(t, err)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Later it would panic leading to a red herring because there's no resp.

if resp != nil {
defer resp.Body.Close()
}
Expand Down
1 change: 1 addition & 0 deletions web/dist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Keeping this file as a placeholder for //got:embed dist to work
6 changes: 5 additions & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gitea Enterprise Installer</title>
<title>{{ .Title }}</title>
<!-- Sets the initial state to be injected fom the server side -->
<script>
window.__INITIAL_STATE__ = {{ .InitialState }};
</script>
</head>
<body>
<div id="root"></div>
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/common/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import React from 'react';
import { useBranding } from '../../contexts/BrandingContext';

export const AppIcon: React.FC<{ className?: string }> = ({ className = 'w-6 h-6' }) => {
const { branding } = useBranding();
if (!branding?.appIcon) {
const { icon } = useBranding();
if (!icon) {
return <div className="h-6 w-6 bg-gray-200 rounded"></div>;
}
return (
<img
src={branding?.appIcon}
src={icon}
alt="App Icon"
className={className}
/>
Expand Down
20 changes: 10 additions & 10 deletions web/src/components/wizard/CompletionStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { CheckCircle, ExternalLink, Copy, ClipboardCheck } from 'lucide-react';

const CompletionStep: React.FC = () => {
const { config, prototypeSettings } = useConfig();
const { branding } = useBranding();
const { title } = useBranding();
const [copied, setCopied] = useState(false);
const themeColor = prototypeSettings.themeColor;

const baseUrl = `${config.useHttps ? 'https' : 'http'}://${config.domain}`;
const urls = [
{ name: 'Web Interface', url: baseUrl, description: `Access the main ${branding?.appTitle} interface` },
{ name: 'API Documentation', url: `${baseUrl}/api/swagger`, description: `Browse and test the ${branding?.appTitle} API` }
{ name: 'Web Interface', url: baseUrl, description: `Access the main ${title} interface` },
{ name: 'API Documentation', url: `${baseUrl}/api/swagger`, description: `Browse and test the ${title} API` }
];

const copyToClipboard = (text: string) => {
Expand All @@ -33,9 +33,9 @@ const CompletionStep: React.FC = () => {
</div>
<h2 className="text-3xl font-bold text-gray-900 mb-4">Installation Complete!</h2>
<p className="text-xl text-gray-600 max-w-2xl mb-8">
{branding?.appTitle} is installed successfully.
{title} is installed successfully.
</p>

<Button
size="lg"
onClick={() => window.open(`${baseUrl}/admin`, '_blank')}
Expand Down Expand Up @@ -78,7 +78,7 @@ const CompletionStep: React.FC = () => {
<Card>
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900">Next Steps</h3>

<div className="space-y-4">
<div className="flex items-start">
<div className="flex-shrink-0 mt-1">
Expand All @@ -87,9 +87,9 @@ const CompletionStep: React.FC = () => {
</div>
</div>
<div className="ml-3">
<h4 className="text-base font-medium text-gray-900">Log in to your {branding?.appTitle} instance</h4>
<h4 className="text-base font-medium text-gray-900">Log in to your {title} instance</h4>
<p className="text-sm text-gray-600 mt-1">
Use the administrator credentials you provided during setup to log in to your {branding?.appTitle} instance.
Use the administrator credentials you provided during setup to log in to your {title} instance.
</p>
</div>
</div>
Expand All @@ -103,7 +103,7 @@ const CompletionStep: React.FC = () => {
<div className="ml-3">
<h4 className="text-base font-medium text-gray-900">Configure additional settings</h4>
<p className="text-sm text-gray-600 mt-1">
Visit the Admin Dashboard to configure additional settings such as authentication providers,
Visit the Admin Dashboard to configure additional settings such as authentication providers,
webhooks, and other integrations.
</p>
</div>
Expand All @@ -129,4 +129,4 @@ const CompletionStep: React.FC = () => {
);
};

export default CompletionStep;
export default CompletionStep;
4 changes: 1 addition & 3 deletions web/src/components/wizard/InstallWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import ValidationInstallStep from './ValidationInstallStep';
import { WizardStep } from '../../types';
import { AppIcon } from '../common/Logo';
import { useWizardMode } from '../../contexts/WizardModeContext';
import { useBranding } from '../../contexts/BrandingContext';

const InstallWizard: React.FC = () => {
const [currentStep, setCurrentStep] = useState<WizardStep>('welcome');
const { text } = useWizardMode();
const { branding } = useBranding();

const goToNextStep = () => {
const steps: WizardStep[] = ['welcome', 'setup', 'installation'];
Expand Down Expand Up @@ -70,4 +68,4 @@ const InstallWizard: React.FC = () => {
);
};

export default InstallWizard;
export default InstallWizard;
Loading
Loading