Skip to content

Commit 9d6f19c

Browse files
authored
Migrate to alpine js (#342)
* WIP - switch to alpinejs Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Bundle alpinejs and move a data driver templatized approach Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix accordiong opening/closing Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix rounded corners for first and last element in accordion Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix text sizing and placement of icons Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Move icon left of text Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix tooltip icon positioning Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix back the kairos logo svg Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix icons Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Close other sections when one opens Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Show models as 2 columns and fix tooltips of initially closed sections Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix artifacts section multi-select Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Show validation errors Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Add modal html Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix multi-select again Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Remove relic from previous experiments Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Migrate modal to alpine.js Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Remove non-standard form manipulation and stick to FormData Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Respect user's architecture choice Fixes kairos-io/kairos#3586 Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Remove legacy manipulation Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Remove unnecessary global state pollution Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Implement popovers with alpine.js and remove flowbite dep Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Convert to pure Alpine.js architecture - Remove mixed document.querySelector/Alpine.$data patterns - Replace DOMContentLoaded wrapper with reactive Alpine.js patterns - Use Alpine.store for component communication - Convert field focusing to x-ref pattern - Eliminate duplicate legacy form submission code Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix test missing Architecture Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix cypress tests Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Remove flowbite relics Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Change base_image to a new type of field It's now just a text box with helper buttons to fill-in predefined values. The user can either edit those or replace completely with their own base image. It simplifies the logic by a lot because, after all, it's only one value we need, so there should be only one input field for it. Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Fix tests Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Make architecture field mandatory and remove the nonsense default Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Move svgs to separate files Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Remove bundled alpine.js and stick to npm Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Change duplicate icons to unique ones Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> * Remove unused code Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me> --------- Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
1 parent 77dfd9c commit 9d6f19c

24 files changed

+1482
-2804
lines changed

e2e/web/cypress/e2e/scpec.cy.js

Lines changed: 83 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ describe('Kairos Factory Web Interface', () => {
33
cy.visit('/')
44
// Wait for the page to be fully loaded
55
cy.get('#accordion-collapse').should('exist')
6+
// Wait for Alpine.js to initialize the accordion sections
7+
cy.get('[id^="accordion-heading-"]').should('have.length.at.least', 4)
68
})
79

810
const expectedText = [
@@ -21,45 +23,62 @@ describe('Kairos Factory Web Interface', () => {
2123
it('should have all required accordion sections', () => {
2224
// Check for the main accordion container
2325
cy.get('#accordion-collapse').should('exist');
24-
25-
// Check for required sections
26+
27+
// Check for required sections (these are always visible)
2628
cy.get('#accordion-heading-base-image').should('exist');
2729
cy.get('#accordion-heading-architecture').should('exist');
2830
cy.get('#accordion-heading-model').should('exist');
2931
cy.get('#accordion-heading-variant').should('exist');
32+
cy.get('#accordion-heading-version').should('exist');
33+
cy.get('#accordion-heading-configuration').should('exist');
34+
cy.get('#accordion-heading-artifacts').should('exist');
35+
36+
// Kubernetes sections are conditional - need to select Standard variant first
37+
// First open the variant section
38+
cy.get('#accordion-heading-variant button').click();
39+
cy.get('label[for="option-standard"]').click();
3040
cy.get('#accordion-heading-kubernetes').should('exist');
3141
cy.get('#accordion-heading-kubernetes-release').should('exist');
32-
cy.get('#accordion-heading-version').should('exist');
3342
});
3443

3544
it('should have all required accordion bodies', () => {
3645
cy.get('#accordion-body-base-image').should('exist');
3746
cy.get('#accordion-body-architecture').should('exist');
3847
cy.get('#accordion-body-model').should('exist');
3948
cy.get('#accordion-body-variant').should('exist');
49+
cy.get('#accordion-body-version').should('exist');
50+
cy.get('#accordion-body-configuration').should('exist');
51+
cy.get('#accordion-body-artifacts').should('exist');
52+
53+
// Kubernetes sections are conditional
54+
cy.get('#accordion-heading-variant button').click();
55+
cy.get('label[for="option-standard"]').click();
4056
cy.get('#accordion-body-kubernetes').should('exist');
4157
cy.get('#accordion-body-kubernetes-release').should('exist');
42-
cy.get('#accordion-body-version').should('exist');
4358
});
4459

4560
it('should allow section interaction', () => {
4661
// Helper function to check section state
4762
const checkSectionState = (headingId, bodyId, shouldBeExpanded) => {
4863
cy.get(`#${headingId} button`).should('have.attr', 'aria-expanded', shouldBeExpanded.toString());
49-
cy.get(`#${bodyId}`).should(shouldBeExpanded ? 'not.have.class' : 'have.class', 'hidden');
64+
if (shouldBeExpanded) {
65+
cy.get(`#${bodyId}`).should('be.visible');
66+
} else {
67+
cy.get(`#${bodyId}`).should('not.be.visible');
68+
}
5069
};
5170

5271
// Check initial state - first section should be expanded
5372
checkSectionState('accordion-heading-base-image', 'accordion-body-base-image', true);
5473
checkSectionState('accordion-heading-architecture', 'accordion-body-architecture', false);
5574

5675
// Click on architecture section
57-
cy.get('#accordion-heading-architecture button').click();
76+
cy.get('#accordion-heading-architecture button').first().click();
5877
checkSectionState('accordion-heading-architecture', 'accordion-body-architecture', true);
5978
checkSectionState('accordion-heading-base-image', 'accordion-body-base-image', false);
6079

6180
// Click on model section
62-
cy.get('#accordion-heading-model button').click();
81+
cy.get('#accordion-heading-model button').first().click();
6382
checkSectionState('accordion-heading-model', 'accordion-body-model', true);
6483
checkSectionState('accordion-heading-architecture', 'accordion-body-architecture', false);
6584
});
@@ -70,34 +89,39 @@ describe('Kairos Factory Web Interface', () => {
7089
cy.get(`input[type="radio"][name="${name}"][value="${value}"]`).should('be.checked');
7190
};
7291

73-
// Select base image
74-
cy.get('label[for="ubuntu-option"]').click();
75-
checkRadioSelection('base_image', 'ubuntu:24.04');
92+
// Helper function to check text input value
93+
const checkTextInputValue = (name, value) => {
94+
cy.get(`input[type="text"][name="${name}"]`).should('have.value', value);
95+
};
96+
97+
// Select base image using the new button-based system (Ubuntu is default)
98+
cy.get('button').contains('Ubuntu 24.04 LTS').click();
99+
checkTextInputValue('base_image', 'ubuntu:24.04');
76100

77-
// Select architecture
78-
cy.get('#accordion-heading-architecture button').click();
79-
cy.get('label[for="amd64-option"]').click();
101+
// Select architecture (AMD64 is selected by default)
102+
cy.get('#accordion-heading-architecture button').first().click();
103+
cy.get('label[for="option-amd64"]').click();
80104
checkRadioSelection('architecture', 'amd64');
81105

82-
// Select model
83-
cy.get('#accordion-heading-model button').click();
84-
cy.get('label[for="generic-option"]').click();
106+
// Select model (Generic is selected by default)
107+
cy.get('#accordion-heading-model button').first().click();
108+
cy.get('label[for="option-generic"]').click();
85109
checkRadioSelection('model', 'generic');
86110
});
87111

88112
it('should handle form submission', () => {
89113
// Intercept the build start request
90114
cy.intercept('POST', '/start').as('startBuild');
91115

92-
// Fill out required fields
93-
cy.get('label[for="ubuntu-option"]').click();
94-
cy.get('#accordion-heading-architecture button').click();
95-
cy.get('label[for="amd64-option"]').click();
96-
cy.get('#accordion-heading-model button').click();
97-
cy.get('label[for="generic-option"]').click();
98-
cy.get('#accordion-heading-variant button').click();
99-
cy.get('label[for="core-option"]').click();
100-
cy.get('#accordion-heading-version button').click();
116+
// Fill out required fields using new Alpine.js structure
117+
cy.get('button').contains('Ubuntu 24.04 LTS').click();
118+
cy.get('#accordion-heading-architecture button').first().click();
119+
cy.get('label[for="option-amd64"]').click();
120+
cy.get('#accordion-heading-model button').first().click();
121+
cy.get('label[for="option-generic"]').click();
122+
cy.get('#accordion-heading-variant button').first().click();
123+
cy.get('label[for="option-core"]').click();
124+
cy.get('#accordion-heading-version button').first().click();
101125
cy.get('#version').type('v0.1.0-alpha');
102126

103127
// Submit form
@@ -106,64 +130,53 @@ describe('Kairos Factory Web Interface', () => {
106130
// Wait for the build start request to complete
107131
cy.wait('@startBuild');
108132

109-
// Polling function to check for visibility at increasing intervals
110-
function checkBuildingContainerImage(attempt = 1) {
111-
const waitTimes = [5000, 15000, 30000, 60000];
112-
if (attempt > waitTimes.length) {
113-
// Final fail if not visible after all attempts
114-
cy.get('#building-container-image').should('be.visible');
115-
return;
116-
}
117-
cy.wait(waitTimes[attempt - 1]).then(() => {
118-
cy.get('#building-container-image').then($el => {
119-
if (Cypress.dom.isVisible($el)) {
120-
// Element is visible, test passes
121-
expect(Cypress.dom.isVisible($el)).to.be.true;
122-
} else {
123-
// Try again with the next wait time
124-
checkBuildingContainerImage(attempt + 1);
125-
}
126-
});
127-
});
128-
}
129-
130-
// Check if modal appears
131-
cy.get('#static-modal').should('be.visible');
132-
checkBuildingContainerImage();
133+
// Check if modal appears (using different selector based on Alpine.js modal)
134+
cy.get('[x-show="isModalVisible"]').should('be.visible');
135+
136+
// Check for the specific building step in the new modal structure
137+
cy.get('#waiting-for-worker').should('be.visible');
133138
});
134139

135140
it('should show ARM-specific options when ARM64 is selected', () => {
136141
// Select ARM64 architecture
137-
cy.get('#accordion-heading-architecture button').click();
138-
cy.get('label[for="arm64-option"]').click();
142+
cy.get('#accordion-heading-architecture button').first().click();
143+
cy.get('label[for="option-arm64"]').click();
139144
// Open the model accordion section
140-
cy.get('#accordion-heading-model button').click();
141-
// Check if ARM-specific model options are visible
142-
cy.get('.model-option.arm-only').each($el => {
143-
cy.wrap($el).should('be.visible');
144-
});
145+
cy.get('#accordion-heading-model button').first().click();
146+
// Check if ARM-specific model options are visible (Raspberry Pi models)
147+
cy.get('label[for="option-rpi3"]').should('be.visible');
148+
cy.get('label[for="option-rpi4"]').should('be.visible');
149+
cy.get('label[for="option-nvidia-agx-orin"]').should('be.visible');
145150
});
146151

147152
it('should hide ARM-specific options when AMD64 is selected', () => {
148-
// Select AMD64 architecture
149-
cy.get('#accordion-heading-architecture button').click();
150-
cy.get('label[for="amd64-option"]').click();
151-
152-
// Check if ARM-specific model options are hidden
153-
cy.get('.model-option.arm-only').should('have.class', 'hidden');
153+
// Select AMD64 architecture (default)
154+
cy.get('#accordion-heading-architecture button').first().click();
155+
cy.get('label[for="option-amd64"]').click();
156+
cy.get('#accordion-heading-model button').first().click();
157+
158+
// ARM-specific model options should not be present in the DOM for AMD64
159+
cy.get('label[for="option-rpi3"]').should('not.exist');
160+
cy.get('label[for="option-rpi4"]').should('not.exist');
161+
cy.get('label[for="option-nvidia-agx-orin"]').should('not.exist');
154162
});
155163

156164
it('should handle BYOI (Bring Your Own Image) option', () => {
157-
// Select BYOI option
158-
cy.get('label[for="byoi-option"]').click();
159-
160-
// Check if BYOI input field is visible and enabled
161-
cy.get('#byoi').should('be.visible').and('be.enabled');
165+
// Check if base image input field is visible and enabled
166+
cy.get('#base_image').should('be.visible').and('be.enabled');
162167

163-
// Enter custom image
164-
cy.get('#byoi').type('custom-repo.com/image:tag');
168+
// Clear the default value and enter custom image
169+
cy.get('#base_image').clear().type('custom-repo.com/image:tag');
165170

166171
// Verify the value was entered
167-
cy.get('#byoi').should('have.value', 'custom-repo.com/image:tag');
172+
cy.get('#base_image').should('have.value', 'custom-repo.com/image:tag');
173+
174+
// Test that clicking a helper button updates the field
175+
cy.get('button').contains('Fedora 40').click();
176+
cy.get('#base_image').should('have.value', 'fedora:40');
177+
178+
// Test that we can edit after clicking a button
179+
cy.get('#base_image').clear().type('my-custom:latest');
180+
cy.get('#base_image').should('have.value', 'my-custom:latest');
168181
});
169182
});

internal/web/api_handlers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ func HandleQueueBuild(c echo.Context) error {
4242
}
4343

4444
// Validate required fields
45-
if req.Version == "" || req.Image == "" {
46-
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Missing required fields (Version, Image)"})
45+
if req.Version == "" || req.Image == "" || req.Architecture == "" {
46+
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Missing required fields (Version, Image, Architecture)"})
4747
}
4848

4949
id, err := uuid.NewV4()

0 commit comments

Comments
 (0)