feat(canvas): show live workflow history and simplify sharing (#3877)
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/kandev/kandev/internal/plugins/instances"
|
||||
"github.com/kandev/kandev/internal/plugins/webapp"
|
||||
taskmodels "github.com/kandev/kandev/internal/task/models"
|
||||
taskservice "github.com/kandev/kandev/internal/task/service"
|
||||
)
|
||||
|
||||
type transitionDataSource interface {
|
||||
ListTaskStepTransitions(context.Context, string, int, string) ([]taskmodels.StepTransition, string, error)
|
||||
ListWorkflowTransitionGroups(context.Context, string, int, string) ([]taskmodels.TransitionGroup, string, error)
|
||||
}
|
||||
|
||||
type webAppTransition struct {
|
||||
ID string `json:"id"`
|
||||
FromWorkflowID *string `json:"from_workflow_id"`
|
||||
FromWorkflowStepID *string `json:"from_workflow_step_id"`
|
||||
ToWorkflowID *string `json:"to_workflow_id"`
|
||||
ToWorkflowStepID *string `json:"to_workflow_step_id"`
|
||||
Trigger string `json:"trigger"`
|
||||
OccurredAt string `json:"occurred_at"`
|
||||
}
|
||||
|
||||
type webAppTransitionGroup struct {
|
||||
Kind string `json:"kind"`
|
||||
FromStepID *string `json:"from_step_id"`
|
||||
ToStepID *string `json:"to_step_id"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (s *Service) listWebAppTaskTransitions(ctx context.Context, w http.ResponseWriter, r *http.Request, host *pluginHost, binding webapp.CapabilityBinding, taskID string) {
|
||||
if !validWebAppKey(taskID) {
|
||||
writeWebAppError(w, http.StatusNotFound, "not_found")
|
||||
return
|
||||
}
|
||||
page, err := webAppRequestPage(r)
|
||||
if err != nil {
|
||||
writeWebAppError(w, http.StatusBadRequest, "invalid_request")
|
||||
return
|
||||
}
|
||||
task, err := host.fetchTaskForScopeCheck(ctx, taskID)
|
||||
if err != nil {
|
||||
writeWebAppError(w, webAppProtocolStatus(err), webAppErrorCode(err))
|
||||
return
|
||||
}
|
||||
if task == nil {
|
||||
writeWebAppError(w, http.StatusNotFound, "not_found")
|
||||
return
|
||||
}
|
||||
if !webAppTaskMatches(ctx, host, binding, *task) {
|
||||
writeWebAppError(w, http.StatusNotFound, "not_found")
|
||||
return
|
||||
}
|
||||
source, ok := s.taskData.(transitionDataSource)
|
||||
if !ok {
|
||||
writeWebAppError(w, http.StatusNotImplemented, "not_implemented")
|
||||
return
|
||||
}
|
||||
items, next, err := source.ListTaskStepTransitions(ctx, taskID, int(page.Limit), page.Cursor)
|
||||
if err != nil {
|
||||
writeTransitionReadError(w, err)
|
||||
return
|
||||
}
|
||||
result := make([]webAppTransition, len(items))
|
||||
for i, item := range items {
|
||||
result[i] = webAppTransition{
|
||||
ID: strconv.FormatInt(item.ID, 10), FromWorkflowID: item.FromWorkflowID,
|
||||
FromWorkflowStepID: item.FromWorkflowStepID, ToWorkflowID: item.ToWorkflowID,
|
||||
ToWorkflowStepID: item.ToWorkflowStepID, Trigger: item.Trigger,
|
||||
OccurredAt: item.OccurredAt.UTC().Format(time.RFC3339Nano),
|
||||
}
|
||||
}
|
||||
writeWebAppJSON(w, r, http.StatusOK, webAppPage[webAppTransition]{Items: result, PageInfo: webAppPageInfo{NextCursor: next, HasMore: next != ""}})
|
||||
}
|
||||
|
||||
func (s *Service) listWebAppWorkflowTransitionGroups(ctx context.Context, w http.ResponseWriter, r *http.Request, host *pluginHost, binding webapp.CapabilityBinding, workflowID string) {
|
||||
if !validWebAppKey(workflowID) {
|
||||
writeWebAppError(w, http.StatusNotFound, "not_found")
|
||||
return
|
||||
}
|
||||
if binding.ScopeKind != instances.ScopeWorkspace || binding.WorkspaceID == "" || !host.capabilities.CanRead(resourceTasks) || !host.capabilities.CanRead(resourceWorkflows) {
|
||||
writeWebAppError(w, http.StatusForbidden, "plugin_permission_denied")
|
||||
return
|
||||
}
|
||||
page, err := webAppRequestPage(r)
|
||||
if err != nil {
|
||||
writeWebAppError(w, http.StatusBadRequest, "invalid_request")
|
||||
return
|
||||
}
|
||||
if host.workflows == nil {
|
||||
writeWebAppError(w, http.StatusNotImplemented, "not_implemented")
|
||||
return
|
||||
}
|
||||
allowed, err := webAppWorkflowAllowed(ctx, host, binding.WorkspaceID, workflowID)
|
||||
if err != nil {
|
||||
writeWebAppError(w, webAppProtocolStatus(err), webAppErrorCode(err))
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
writeWebAppError(w, http.StatusNotFound, "not_found")
|
||||
return
|
||||
}
|
||||
source, ok := s.taskData.(transitionDataSource)
|
||||
if !ok {
|
||||
writeWebAppError(w, http.StatusNotImplemented, "not_implemented")
|
||||
return
|
||||
}
|
||||
items, next, err := source.ListWorkflowTransitionGroups(ctx, workflowID, int(page.Limit), page.Cursor)
|
||||
if err != nil {
|
||||
writeTransitionReadError(w, err)
|
||||
return
|
||||
}
|
||||
result := make([]webAppTransitionGroup, len(items))
|
||||
for i, item := range items {
|
||||
result[i] = webAppTransitionGroup{Kind: item.Kind, FromStepID: item.FromStepID, ToStepID: item.ToStepID, Count: item.Count}
|
||||
}
|
||||
writeWebAppJSON(w, r, http.StatusOK, webAppPage[webAppTransitionGroup]{Items: result, PageInfo: webAppPageInfo{NextCursor: next, HasMore: next != ""}})
|
||||
}
|
||||
|
||||
func webAppWorkflowAllowed(ctx context.Context, host *pluginHost, workspaceID, workflowID string) (bool, error) {
|
||||
workflows, err := host.workflows.ListWorkflows(ctx, workspaceID, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, workflow := range workflows {
|
||||
if workflow != nil && workflow.ID == workflowID && workflow.WorkspaceID == workspaceID {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func writeTransitionReadError(w http.ResponseWriter, err error) {
|
||||
if errors.Is(err, taskservice.ErrInvalidTransitionCursor) {
|
||||
writeWebAppError(w, http.StatusBadRequest, "invalid_request")
|
||||
return
|
||||
}
|
||||
writeWebAppError(w, webAppProtocolStatus(err), webAppErrorCode(err))
|
||||
}
|
||||
Reference in New Issue
Block a user