Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions frontend/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,13 @@ export const API = {
/**
* Activates a specific version.
* @param {string} functionId - Function ID
* @param {number} version - Version number to activate
* @returns {Promise<LunarFunction>} The updated function
* @param {string} versionId - Version ID to activate
* @returns {Promise<void>}
*/
activate: (functionId, version) =>
activate: (functionId, versionId) =>
apiRequest({
method: "POST",
url: `/api/functions/${functionId}/versions/${version}/activate`,
url: `/api/functions/${functionId}/versions/${versionId}/activate`,
}),

/**
Expand All @@ -236,6 +236,18 @@ export const API = {
method: "GET",
url: `/api/functions/${functionId}/diff/${v1}/${v2}`,
}),

/**
* Deletes a specific version.
* @param {string} functionId - Function ID
* @param {string} versionId - Version ID to delete
* @returns {Promise<void>}
*/
delete: (functionId, versionId) =>
apiRequest({
method: "DELETE",
url: `/api/functions/${functionId}/versions/${versionId}`,
}),
},

/**
Expand Down
5 changes: 5 additions & 0 deletions frontend/js/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,11 @@ export default {
selectToCompare: "Select 2 versions to compare",
compareVersions: "Compare v{{v1}} and v{{v2}}",
versionsCount: "{{count}} versions",
deleteConfirm:
"Delete version {{version}}? This will also delete all executions for this version. This action cannot be undone.",
versionDeleted: "Version {{version}} deleted",
failedToDelete: "Failed to delete version",
delete: "Delete",
},

// AI Request viewer
Expand Down
5 changes: 5 additions & 0 deletions frontend/js/i18n/locales/pt-BR.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,11 @@ export default {
selectToCompare: "Selecione 2 versões para comparar",
compareVersions: "Comparar v{{v1}} e v{{v2}}",
versionsCount: "{{count}} versões",
deleteConfirm:
"Excluir versão {{version}}? Isso também excluirá todas as execuções desta versão. Esta ação não pode ser desfeita.",
versionDeleted: "Versão {{version}} excluída",
failedToDelete: "Falha ao excluir versão",
delete: "Excluir",
},

// AI Request viewer
Expand Down
81 changes: 62 additions & 19 deletions frontend/js/views/function-versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,20 +187,51 @@ export const FunctionVersions = {

/**
* Activates a specific version.
* @param {number} version - Version number to activate
* @param {FunctionVersion} ver - Version object to activate
* @returns {Promise<void>}
*/
activateVersion: async (version) => {
if (!confirm(t("versionsPage.activateConfirm", { version }))) return;
activateVersion: async (ver) => {
if (!confirm(t("versionsPage.activateConfirm", { version: ver.version }))) {
return;
}
try {
await API.versions.activate(FunctionVersions.func.id, version);
Toast.show(t("versionsPage.versionActivated", { version }), "success");
await API.versions.activate(FunctionVersions.func.id, ver.id);
Toast.show(
t("versionsPage.versionActivated", { version: ver.version }),
"success",
);
await FunctionVersions.loadData(FunctionVersions.func.id);
} catch (e) {
Toast.show(t("versionsPage.failedToActivate"), "error");
}
},

/**
* Deletes a specific version.
* @param {FunctionVersion} ver - Version object to delete
* @returns {Promise<void>}
*/
deleteVersion: async (ver) => {
if (!confirm(t("versionsPage.deleteConfirm", { version: ver.version }))) {
return;
}
try {
await API.versions.delete(FunctionVersions.func.id, ver.id);
Toast.show(
t("versionsPage.versionDeleted", { version: ver.version }),
"success",
);
// Remove from selected versions if it was selected
const idx = FunctionVersions.selectedVersions.indexOf(ver.version);
if (idx !== -1) {
FunctionVersions.selectedVersions.splice(idx, 1);
}
await FunctionVersions.loadVersions();
} catch (e) {
Toast.show(t("versionsPage.failedToDelete"), "error");
}
},

/**
* Navigates to the version diff view for selected versions.
*/
Expand Down Expand Up @@ -345,21 +376,33 @@ export const FunctionVersions = {
]),
m(TableCell, formatUnixTimestamp(ver.created_at)),
m(TableCell, { align: "right" }, [
ver.version !== func.active_version.version &&
m(
Button,
{
variant: ButtonVariant.OUTLINE,
size: ButtonSize.SM,
onclick: (e) => {
e.stopPropagation();
FunctionVersions.activateVersion(
ver.version,
);
ver.version !== func.active_version.version && [
m(
Button,
{
variant: ButtonVariant.OUTLINE,
size: ButtonSize.SM,
onclick: (e) => {
e.stopPropagation();
FunctionVersions.activateVersion(ver);
},
},
},
t("versionsPage.activate"),
),
t("versionsPage.activate"),
),
m(
Button,
{
variant: ButtonVariant.DESTRUCTIVE,
size: ButtonSize.SM,
style: "margin-left: 0.5rem;",
onclick: (e) => {
e.stopPropagation();
FunctionVersions.deleteVersion(ver);
},
},
t("versionsPage.delete"),
),
],
]),
],
)
Expand Down
74 changes: 69 additions & 5 deletions internal/api/docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -487,21 +487,21 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"

/api/functions/{id}/versions/{version}/activate:
/api/functions/{id}/versions/{versionId}/activate:
parameters:
- name: id
in: path
required: true
description: Unique identifier of the function
schema:
type: string
- name: version
- name: versionId
in: path
required: true
description: Version number to activate
description: Unique identifier of the version (primary key)
schema:
type: integer
minimum: 1
type: string
example: "ver_abc123_v1"

post:
tags:
Expand All @@ -518,6 +518,70 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Version not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"500":
description: Internal server error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"

/api/functions/{id}/versions/{versionId}:
parameters:
- name: id
in: path
required: true
description: Unique identifier of the function
schema:
type: string
- name: versionId
in: path
required: true
description: Unique identifier of the version (primary key)
schema:
type: string
example: "ver_abc123_v1"

delete:
tags:
- Versions
summary: Delete a version
description: |
Permanently deletes a specific version of a function.
The active version cannot be deleted - you must activate a different version first.
Deleting a version will also delete all executions associated with that version.
operationId: deleteVersion
responses:
"204":
description: Version deleted successfully
"400":
description: Cannot delete active version
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
activeVersion:
summary: Attempting to delete active version
value:
error: "Cannot delete active version"
"401":
description: Authentication required
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Version not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"500":
description: Internal server error
content:
Expand Down
39 changes: 29 additions & 10 deletions internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,23 +417,42 @@ func GetVersionHandler(database store.DB) http.HandlerFunc {
// ActivateVersionHandler returns a handler for activating a version
func ActivateVersionHandler(database store.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
versionStr := r.PathValue("version")
versionID := r.PathValue("versionId")

// Parse version number
versionNum, err := strconv.Atoi(versionStr)
if err != nil {
writeError(w, http.StatusBadRequest, "Invalid version number")
// Activate the version
if err := database.ActivateVersion(r.Context(), versionID); err != nil {
if err == store.ErrVersionNotFound {
writeError(w, http.StatusNotFound, "Version not found")
return
}
writeError(w, http.StatusInternalServerError, "Failed to activate version")
return
}

// Activate the version
if err := database.ActivateVersion(r.Context(), id, versionNum); err != nil {
writeError(w, http.StatusNotFound, "Version not found")
w.WriteHeader(http.StatusOK)
}
}

// DeleteVersionHandler returns a handler for deleting a version
func DeleteVersionHandler(database store.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
versionID := r.PathValue("versionId")

// Delete the version
if err := database.DeleteVersion(r.Context(), versionID); err != nil {
if err == store.ErrVersionNotFound {
writeError(w, http.StatusNotFound, "Version not found")
return
}
if err == store.ErrCannotDeleteActiveVersion {
writeError(w, http.StatusBadRequest, "Cannot delete active version")
return
}
writeError(w, http.StatusInternalServerError, "Failed to delete version")
return
}

w.WriteHeader(http.StatusOK)
w.WriteHeader(http.StatusNoContent)
}
}

Expand Down
3 changes: 2 additions & 1 deletion internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ func (s *Server) setupRoutes() {
// Version Management - only need DB
s.mux.Handle("GET /api/functions/{id}/versions", authMiddleware(http.HandlerFunc(ListVersionsHandler(s.db))))
s.mux.Handle("GET /api/functions/{id}/versions/{version}", authMiddleware(http.HandlerFunc(GetVersionHandler(s.db))))
s.mux.Handle("POST /api/functions/{id}/versions/{version}/activate", authMiddleware(http.HandlerFunc(ActivateVersionHandler(s.db))))
s.mux.Handle("POST /api/functions/{id}/versions/{versionId}/activate", authMiddleware(http.HandlerFunc(ActivateVersionHandler(s.db))))
s.mux.Handle("DELETE /api/functions/{id}/versions/{versionId}", authMiddleware(http.HandlerFunc(DeleteVersionHandler(s.db))))
s.mux.Handle("GET /api/functions/{id}/diff/{v1}/{v2}", authMiddleware(http.HandlerFunc(GetVersionDiffHandler(s.db))))

// Execution History - only need DB
Expand Down
5 changes: 3 additions & 2 deletions internal/api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,11 @@ func TestActivateVersion(t *testing.T) {

// Create a test function and two versions
fn := createTestFunction(t, database)
createTestVersion(t, database, fn.ID, "function handler(ctx, event)\n return {statusCode = 200}\nend")
v1 := createTestVersion(t, database, fn.ID, "function handler(ctx, event)\n return {statusCode = 200}\nend")
createTestVersion(t, database, fn.ID, "function handler(ctx, event)\n return {statusCode = 201}\nend")

req := makeAuthRequest(http.MethodPost, "/api/functions/"+fn.ID+"/versions/1/activate", nil)
// Use version ID (primary key) instead of version number
req := makeAuthRequest(http.MethodPost, "/api/functions/"+fn.ID+"/versions/"+v1.ID+"/activate", nil)
w := httptest.NewRecorder()

server.Handler().ServeHTTP(w, req)
Expand Down
Loading
Loading