Skip to content

Commit 8967af3

Browse files
authored
feat(web): initial work to setup root handler in the web component (#2192)
* feat(web): initial work to setup root handler * chore: rework how we expose the web routes * fix: paths * chore: cleanup of unused logic * chore: add tests * chore: add removed web/dist/README.md by mistake * chore: s/README.md/.gitkeep * chore: turns hidden files aren't cool for embedding * chore: missed gofmt for wtv reason * chore: refactor web a bit * chore: allow overriding the web's fs * chore: actually run all of the unit tests
1 parent 852e871 commit 8967af3

File tree

19 files changed

+628
-315
lines changed

19 files changed

+628
-315
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,9 @@ envtest:
274274
.PHONY: unit-tests
275275
unit-tests: envtest
276276
KUBEBUILDER_ASSETS="$(shell ./operator/bin/setup-envtest use $(ENVTEST_K8S_VERSION) --bin-dir $(shell pwd)/operator/bin -p path)" \
277-
go test -tags $(GO_BUILD_TAGS) -v ./pkg/... ./cmd/...
277+
go test -tags $(GO_BUILD_TAGS) -v ./pkg/... ./cmd/... ./api/... ./web/... ./pkg-new/...
278278
$(MAKE) -C operator test
279+
$(MAKE) -C utils unit-tests
279280

280281
.PHONY: vet
281282
vet:

api/api.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ func (a *API) RegisterRoutes(router *mux.Router) {
124124
router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler)
125125

126126
router.HandleFunc("/auth/login", a.postAuthLogin).Methods("POST")
127-
router.HandleFunc("/branding", a.getBranding).Methods("GET")
128127

129128
authenticatedRouter := router.PathPrefix("/").Subrouter()
130129
authenticatedRouter.Use(a.authMiddleware)

api/console.go

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,8 @@ package api
22

33
import (
44
"net/http"
5-
6-
"github.com/replicatedhq/embedded-cluster/api/types"
75
)
86

9-
type getBrandingResponse struct {
10-
Branding types.Branding `json:"branding"`
11-
}
12-
13-
func (a *API) getBranding(w http.ResponseWriter, r *http.Request) {
14-
branding, err := a.consoleController.GetBranding()
15-
if err != nil {
16-
a.logError(r, err, "failed to get branding")
17-
a.jsonError(w, r, err)
18-
return
19-
}
20-
21-
response := getBrandingResponse{
22-
Branding: branding,
23-
}
24-
25-
a.json(w, r, http.StatusOK, response)
26-
}
27-
287
type getListAvailableNetworkInterfacesResponse struct {
298
NetworkInterfaces []string `json:"networkInterfaces"`
309
}

api/controllers/console/controller.go

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
package console
22

33
import (
4-
"fmt"
5-
64
"github.com/replicatedhq/embedded-cluster/api/pkg/utils"
7-
"github.com/replicatedhq/embedded-cluster/api/types"
8-
"github.com/replicatedhq/embedded-cluster/pkg/release"
95
)
106

117
type Controller interface {
12-
GetBranding() (types.Branding, error)
138
ListAvailableNetworkInterfaces() ([]string, error)
149
}
1510

@@ -41,18 +36,6 @@ func NewConsoleController(opts ...ConsoleControllerOption) (*ConsoleController,
4136
return controller, nil
4237
}
4338

44-
func (c *ConsoleController) GetBranding() (types.Branding, error) {
45-
app := release.GetApplication()
46-
if app == nil {
47-
return types.Branding{}, fmt.Errorf("application not found")
48-
}
49-
50-
return types.Branding{
51-
AppTitle: app.Spec.Title,
52-
AppIcon: app.Spec.Icon,
53-
}, nil
54-
}
55-
5639
func (c *ConsoleController) ListAvailableNetworkInterfaces() ([]string, error) {
5740
return c.NetUtils.ListValidNetworkInterfaces()
5841
}

api/types/console.go

Lines changed: 0 additions & 6 deletions
This file was deleted.

api/types/errors_test.go

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"errors"
66
"fmt"
77
"net/http"
8-
"net/http/httptest"
98
"testing"
109

1110
"github.com/stretchr/testify/assert"
@@ -224,94 +223,3 @@ func TestAPIError_ErrorOrNil(t *testing.T) {
224223
})
225224
}
226225
}
227-
228-
func TestAPIError_JSON(t *testing.T) {
229-
tests := []struct {
230-
name string
231-
apiErr *APIError
232-
wantCode int
233-
wantJSON map[string]any
234-
}{
235-
{
236-
name: "simple error",
237-
apiErr: &APIError{
238-
StatusCode: http.StatusInternalServerError,
239-
Message: "invalid request",
240-
},
241-
wantCode: http.StatusInternalServerError,
242-
wantJSON: map[string]any{
243-
"status_code": float64(http.StatusInternalServerError),
244-
"message": "invalid request",
245-
},
246-
},
247-
{
248-
name: "field error",
249-
apiErr: &APIError{
250-
StatusCode: http.StatusBadRequest,
251-
Message: "validation error",
252-
Field: "username",
253-
},
254-
wantCode: http.StatusBadRequest,
255-
wantJSON: map[string]any{
256-
"status_code": float64(http.StatusBadRequest),
257-
"message": "validation error",
258-
"field": "username",
259-
},
260-
},
261-
{
262-
name: "error with nested errors",
263-
apiErr: &APIError{
264-
StatusCode: http.StatusBadRequest,
265-
Message: "multiple validation errors",
266-
Errors: []*APIError{
267-
{
268-
Message: "field1 is required",
269-
Field: "field1",
270-
},
271-
{
272-
Message: "field2 must be a number",
273-
Field: "field2",
274-
},
275-
},
276-
},
277-
wantCode: http.StatusBadRequest,
278-
wantJSON: map[string]any{
279-
"status_code": float64(http.StatusBadRequest),
280-
"message": "multiple validation errors",
281-
"errors": []any{
282-
map[string]any{
283-
"message": "field1 is required",
284-
"field": "field1",
285-
},
286-
map[string]any{
287-
"message": "field2 must be a number",
288-
"field": "field2",
289-
},
290-
},
291-
},
292-
},
293-
}
294-
295-
for _, tt := range tests {
296-
t.Run(tt.name, func(t *testing.T) {
297-
// Create a mock HTTP response recorder
298-
rec := httptest.NewRecorder()
299-
300-
// Call the JSON method
301-
tt.apiErr.JSON(rec)
302-
303-
// Check status code
304-
assert.Equal(t, tt.wantCode, rec.Code, "Status code should match")
305-
306-
// Check content type header
307-
contentType := rec.Header().Get("Content-Type")
308-
assert.Equal(t, "application/json", contentType, "Content-Type header should be application/json")
309-
310-
// Parse and check the JSON response
311-
var gotJSON map[string]any
312-
err := json.Unmarshal(rec.Body.Bytes(), &gotJSON)
313-
assert.NoError(t, err, "Should be able to parse the JSON response")
314-
assert.Equal(t, tt.wantJSON, gotJSON, "JSON response should match expected structure")
315-
})
316-
}
317-
}

cmd/installer/cli/install.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"io/fs"
910
"log"
1011
"net"
1112
"net/http"
@@ -97,6 +98,9 @@ type InstallCmdFlags struct {
9798
tlsKeyBytes []byte
9899
}
99100

101+
// 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.
102+
var webAssetsFS fs.FS = nil
103+
100104
// InstallCmd returns a cobra command for installing the embedded cluster.
101105
func InstallCmd(ctx context.Context, name string) *cobra.Command {
102106
var flags InstallCmdFlags
@@ -455,16 +459,21 @@ func runInstallAPI(ctx context.Context, listener net.Listener, cert tls.Certific
455459
if err != nil {
456460
return fmt.Errorf("new api: %w", err)
457461
}
462+
app := release.GetApplication()
463+
if app == nil {
464+
return fmt.Errorf("application not found")
465+
}
458466

459-
api.RegisterRoutes(router.PathPrefix("/api").Subrouter())
460-
461-
var webFs http.Handler
462-
if os.Getenv("EC_DEV_ENV") == "true" {
463-
webFs = http.FileServer(http.FS(os.DirFS("./web/dist")))
464-
} else {
465-
webFs = http.FileServer(http.FS(web.Fs()))
467+
webServer, err := web.New(web.InitialState{
468+
Title: app.Spec.Title,
469+
Icon: app.Spec.Icon,
470+
}, web.WithLogger(logger), web.WithAssetsFS(webAssetsFS))
471+
if err != nil {
472+
return fmt.Errorf("new web server: %w", err)
466473
}
467-
router.PathPrefix("/").Methods("GET").Handler(webFs)
474+
475+
api.RegisterRoutes(router.PathPrefix("/api").Subrouter())
476+
webServer.RegisterRoutes(router.PathPrefix("/").Subrouter())
468477

469478
server := &http.Server{
470479
// ErrorLog outputs TLS errors and warnings to the console, we want to make sure we use the same logrus logger for them

cmd/installer/cli/install_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"os"
1313
"path/filepath"
1414
"testing"
15+
"testing/fstest"
1516
"time"
1617

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

564+
// We need a release object to pass over to the Web component.
565+
dataMap := map[string][]byte{
566+
"kots-app.yaml": []byte(`
567+
apiVersion: kots.io/v1beta1
568+
kind: Application
569+
`),
570+
}
571+
err = release.SetReleaseDataForTests(dataMap)
572+
require.NoError(t, err)
573+
574+
t.Cleanup(func() {
575+
release.SetReleaseDataForTests(nil)
576+
})
577+
578+
// Mock the web assets filesystem so that we don't need to embed the web assets.
579+
webAssetsFS = fstest.MapFS{
580+
"index.html": &fstest.MapFile{
581+
Data: []byte(""),
582+
Mode: 0644,
583+
},
584+
}
585+
defer func() { webAssetsFS = nil }()
586+
563587
go func() {
564588
err := runInstallAPI(ctx, listener, cert, logger, "password", nil)
565589
t.Logf("Install API exited with error: %v", err)
@@ -581,7 +605,7 @@ func Test_runInstallAPI(t *testing.T) {
581605
},
582606
}
583607
resp, err := httpClient.Get(url)
584-
assert.NoError(t, err)
608+
require.NoError(t, err)
585609
if resp != nil {
586610
defer resp.Body.Close()
587611
}

web/dist/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Keeping this file as a placeholder for //got:embed dist to work

web/index.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>Gitea Enterprise Installer</title>
7+
<title>{{ .Title }}</title>
8+
<!-- Sets the initial state to be injected fom the server side -->
9+
<script>
10+
window.__INITIAL_STATE__ = {{ .InitialState }};
11+
</script>
812
</head>
913
<body>
1014
<div id="root"></div>

web/src/components/common/Logo.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import React from 'react';
33
import { useBranding } from '../../contexts/BrandingContext';
44

55
export const AppIcon: React.FC<{ className?: string }> = ({ className = 'w-6 h-6' }) => {
6-
const { branding } = useBranding();
7-
if (!branding?.appIcon) {
6+
const { icon } = useBranding();
7+
if (!icon) {
88
return <div className="h-6 w-6 bg-gray-200 rounded"></div>;
99
}
1010
return (
1111
<img
12-
src={branding?.appIcon}
12+
src={icon}
1313
alt="App Icon"
1414
className={className}
1515
/>

web/src/components/wizard/CompletionStep.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import { CheckCircle, ExternalLink, Copy, ClipboardCheck } from 'lucide-react';
77

88
const CompletionStep: React.FC = () => {
99
const { config, prototypeSettings } = useConfig();
10-
const { branding } = useBranding();
10+
const { title } = useBranding();
1111
const [copied, setCopied] = useState(false);
1212
const themeColor = prototypeSettings.themeColor;
1313

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

2020
const copyToClipboard = (text: string) => {
@@ -33,9 +33,9 @@ const CompletionStep: React.FC = () => {
3333
</div>
3434
<h2 className="text-3xl font-bold text-gray-900 mb-4">Installation Complete!</h2>
3535
<p className="text-xl text-gray-600 max-w-2xl mb-8">
36-
{branding?.appTitle} is installed successfully.
36+
{title} is installed successfully.
3737
</p>
38-
38+
3939
<Button
4040
size="lg"
4141
onClick={() => window.open(`${baseUrl}/admin`, '_blank')}
@@ -78,7 +78,7 @@ const CompletionStep: React.FC = () => {
7878
<Card>
7979
<div className="space-y-4">
8080
<h3 className="text-lg font-medium text-gray-900">Next Steps</h3>
81-
81+
8282
<div className="space-y-4">
8383
<div className="flex items-start">
8484
<div className="flex-shrink-0 mt-1">
@@ -87,9 +87,9 @@ const CompletionStep: React.FC = () => {
8787
</div>
8888
</div>
8989
<div className="ml-3">
90-
<h4 className="text-base font-medium text-gray-900">Log in to your {branding?.appTitle} instance</h4>
90+
<h4 className="text-base font-medium text-gray-900">Log in to your {title} instance</h4>
9191
<p className="text-sm text-gray-600 mt-1">
92-
Use the administrator credentials you provided during setup to log in to your {branding?.appTitle} instance.
92+
Use the administrator credentials you provided during setup to log in to your {title} instance.
9393
</p>
9494
</div>
9595
</div>
@@ -103,7 +103,7 @@ const CompletionStep: React.FC = () => {
103103
<div className="ml-3">
104104
<h4 className="text-base font-medium text-gray-900">Configure additional settings</h4>
105105
<p className="text-sm text-gray-600 mt-1">
106-
Visit the Admin Dashboard to configure additional settings such as authentication providers,
106+
Visit the Admin Dashboard to configure additional settings such as authentication providers,
107107
webhooks, and other integrations.
108108
</p>
109109
</div>
@@ -129,4 +129,4 @@ const CompletionStep: React.FC = () => {
129129
);
130130
};
131131

132-
export default CompletionStep;
132+
export default CompletionStep;

0 commit comments

Comments
 (0)