From 9d21badb3e759b409da11589f799ae840d6d3b1d Mon Sep 17 00:00:00 2001 From: Rogerio Pontual Date: Thu, 29 May 2025 13:24:49 +0200 Subject: [PATCH 01/11] Add icon to projects and workflows where the user has a chat --- lib/lightning/projects.ex | 19 ++++++-- lib/lightning/workflows.ex | 43 ++++++++++++++++--- lib/lightning/workflows/workflow.ex | 1 + .../live/dashboard_live/components.ex | 2 +- .../workflow_live/dashboard_components.ex | 2 +- lib/lightning_web/live/workflow_live/index.ex | 3 +- test/lightning/projects_test.exs | 39 +++++++++++------ 7 files changed, 84 insertions(+), 25 deletions(-) diff --git a/lib/lightning/projects.ex b/lib/lightning/projects.ex index 5f3d911db4..5d25ef82d0 100644 --- a/lib/lightning/projects.ex +++ b/lib/lightning/projects.ex @@ -13,6 +13,7 @@ defmodule Lightning.Projects do alias Lightning.Accounts.User alias Lightning.Accounts.UserNotifier alias Lightning.Accounts.UserToken + alias Lightning.AiAssistant.ChatSession alias Lightning.Config alias Lightning.ExportUtils alias Lightning.Invocation.Dataclip @@ -46,7 +47,8 @@ defmodule Lightning.Projects do :role, :workflows_count, :collaborators_count, - :last_updated_at + :last_updated_at, + :has_ai_chat ] end @@ -67,7 +69,7 @@ defmodule Lightning.Projects do role: fragment("'support' as role"), workflows_count: count(w.id, :distinct), collaborators_count: count(pu_all.user_id, :distinct), - last_updated_at: max(w.updated_at) + last_updated_at: max(w.updated_at), } ) |> Repo.all() @@ -95,7 +97,17 @@ defmodule Lightning.Projects do end defp projects_overview_query(user_id) do + chat_session_exists_query = + from cs in ChatSession, + join: j in Job, + on: cs.job_id == j.id, + join: w in Workflow, + on: j.workflow_id == w.id, + where: cs.user_id == ^user_id and w.project_id == parent_as(:project).id and is_nil(w.deleted_at), + select: 1 + from(p in Project, + as: :project, left_join: w in assoc(p, :workflows), inner_join: pu in assoc(p, :project_users), left_join: pu_all in assoc(p, :project_users), @@ -107,7 +119,8 @@ defmodule Lightning.Projects do role: pu.role, workflows_count: count(w.id, :distinct), collaborators_count: count(pu_all.user_id, :distinct), - last_updated_at: max(w.updated_at) + last_updated_at: max(w.updated_at), + has_ai_chat: exists(chat_session_exists_query) } ) end diff --git a/lib/lightning/workflows.ex b/lib/lightning/workflows.ex index 70ee06d529..3df2ba0c11 100644 --- a/lib/lightning/workflows.ex +++ b/lib/lightning/workflows.ex @@ -10,6 +10,8 @@ defmodule Lightning.Workflows do alias Lightning.KafkaTriggers alias Lightning.Projects.Project alias Lightning.Repo + alias Lightning.Accounts.User + alias Lightning.AiAssistant.ChatSession alias Lightning.Workflows.Audit alias Lightning.Workflows.Edge alias Lightning.Workflows.Events @@ -336,22 +338,51 @@ defmodule Lightning.Workflows do query = from(w in Workflow, - where: is_nil(w.deleted_at) and w.project_id == ^project.id, + where: w.project_id == ^project.id and is_nil(w.deleted_at), preload: ^include ) + query + |> maybe_filter_by_name(opts) + |> apply_sorting(order_by) + |> Repo.all() + end + + def get_workflows_for(%Project{id: project_id}, %User{id: user_id}, opts) do + chat_session_exists_query = + from cs in ChatSession, + join: j in Job, + on: cs.job_id == j.id, + join: w in Workflow, + on: j.workflow_id == w.id, + where: cs.user_id == ^user_id and w.project_id == parent_as(:workflow).id and is_nil(w.deleted_at), + select: 1 + + include = Keyword.get(opts, :include, [:triggers]) + order_by = Keyword.get(opts, :order_by, {:name, :asc}) + query = - if search = Keyword.get(opts, :search) do - from w in query, where: ilike(w.name, ^"%#{search}%") - else - query - end + from(w in Workflow, + as: :workflow, + where: w.project_id == ^project_id and is_nil(w.deleted_at), + select: %{w | has_ai_chat: exists(chat_session_exists_query)}, + preload: ^include + ) query + |> maybe_filter_by_name(opts) |> apply_sorting(order_by) |> Repo.all() end + defp maybe_filter_by_name(query, opts) do + if search = Keyword.get(opts, :search) do + where(query, [w], ilike(w.name, ^"%#{search}%")) + else + query + end + end + defp apply_sorting(query, {:name, direction}) when is_atom(direction) do from w in query, order_by: [{^direction, w.name}] end diff --git a/lib/lightning/workflows/workflow.ex b/lib/lightning/workflows/workflow.ex index 4158e67571..025a390b49 100644 --- a/lib/lightning/workflows/workflow.ex +++ b/lib/lightning/workflows/workflow.ex @@ -55,6 +55,7 @@ defmodule Lightning.Workflows.Workflow do field :deleted_at, :utc_datetime field :delete, :boolean, virtual: true + field :has_ai_chat, :boolean, virtual: true timestamps() end diff --git a/lib/lightning_web/live/dashboard_live/components.ex b/lib/lightning_web/live/dashboard_live/components.ex index 3c18f52eb4..dd0149a447 100644 --- a/lib/lightning_web/live/dashboard_live/components.ex +++ b/lib/lightning_web/live/dashboard_live/components.ex @@ -114,7 +114,7 @@ defmodule LightningWeb.DashboardLive.Components do onclick={JS.navigate(~p"/projects/#{project.id}/w")} > <.td> - {project.name} + {project.name} <.icon :if={project.has_ai_chat} name="hero-sparkles" class="size-4"/> <.td class="break-words max-w-[25rem]"> {String.capitalize(to_string(project.role))} diff --git a/lib/lightning_web/live/workflow_live/dashboard_components.ex b/lib/lightning_web/live/workflow_live/dashboard_components.ex index f1f40b68aa..e348c919fe 100644 --- a/lib/lightning_web/live/workflow_live/dashboard_components.ex +++ b/lib/lightning_web/live/workflow_live/dashboard_components.ex @@ -323,7 +323,7 @@ defmodule LightningWeb.WorkflowLive.DashboardComponents do class="flex-shrink truncate text-gray-900 font-medium workflow-name" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" > - {@workflow.name} + {@workflow.name} <.icon :if={@workflow.has_ai_chat} name="hero-sparkles" class="size-4" /> <%= if @trigger_enabled do %> diff --git a/lib/lightning_web/live/workflow_live/index.ex b/lib/lightning_web/live/workflow_live/index.ex index a44623f8c8..559a90ce33 100644 --- a/lib/lightning_web/live/workflow_live/index.ex +++ b/lib/lightning_web/live/workflow_live/index.ex @@ -110,6 +110,7 @@ defmodule LightningWeb.WorkflowLive.Index do defp apply_action(socket, :index, _params) do %{ + current_user: current_user, project: project, search_term: search_term, sort_key: sort_key, @@ -125,7 +126,7 @@ defmodule LightningWeb.WorkflowLive.Index do opts end - workflows = Workflows.get_workflows_for(project, opts) + workflows = Workflows.get_workflows_for(project, current_user, opts) workflow_stats = Enum.map(workflows, &DashboardStats.get_workflow_stats/1) sorted_stats = diff --git a/test/lightning/projects_test.exs b/test/lightning/projects_test.exs index 9f97c31ef6..c8c0e76a34 100644 --- a/test/lightning/projects_test.exs +++ b/test/lightning/projects_test.exs @@ -1468,7 +1468,7 @@ defmodule Lightning.ProjectsTest do assert Projects.get_projects_overview(user) == [] end - test "returns projects overview with workflows and collaborators count" do + test "returns projects overview with workflows, collaborators count and if has chats " do user = insert(:user) other_user = insert(:user) @@ -1477,25 +1477,38 @@ defmodule Lightning.ProjectsTest do insert(:project, name: "Project A", project_users: [%{user_id: user.id}]) insert(:simple_workflow, project: project) - insert(:simple_workflow, project: project) + %{jobs: [job1 | _]} = insert(:simple_workflow, project: project) + + insert(:chat_session, + user: user, + job: job1, + messages: [%{role: :user, content: "what?", user: user}] + ) insert(:project, name: "Project B", project_users: [%{user_id: other_user.id}] ) + |> then(fn other_project -> + %{jobs: [other_job | _]} = + insert(:simple_workflow, project: other_project) - result = Projects.get_projects_overview(user) - - assert length(result) == 1 + insert(:chat_session, + user: other_user, + job: other_job, + messages: [%{role: :user, content: "what?", user: other_user}] + ) + end) - [ - %ProjectOverviewRow{ - id: ^project_id, - name: "Project A", - workflows_count: 2, - collaborators_count: 1 - } - ] = result + assert [ + %ProjectOverviewRow{ + id: ^project_id, + name: "Project A", + workflows_count: 2, + collaborators_count: 1, + has_ai_chat: true + } + ] = Projects.get_projects_overview(user) end test "orders projects by name ascending by default" do From 87cd373ec8a6b7b3533fd2b20987096644736170 Mon Sep 17 00:00:00 2001 From: Rogerio Pontual Date: Fri, 30 May 2025 11:53:13 +0200 Subject: [PATCH 02/11] Add icon to jobs with chats on WorkflowDiagram --- lib/lightning/jobs.ex | 14 ++++++++++++ lib/lightning/projects.ex | 6 +++-- lib/lightning/workflows.ex | 6 +++-- .../live/dashboard_live/components.ex | 7 +++++- .../workflow_live/dashboard_components.ex | 3 ++- lib/lightning_web/live/workflow_live/edit.ex | 22 ++++++++++++++++++- 6 files changed, 51 insertions(+), 7 deletions(-) diff --git a/lib/lightning/jobs.ex b/lib/lightning/jobs.ex index 917b3993cd..b8f4a2c9c7 100644 --- a/lib/lightning/jobs.ex +++ b/lib/lightning/jobs.ex @@ -6,6 +6,9 @@ defmodule Lightning.Jobs do import Ecto.Query alias Ecto.Multi + + alias Lightning.Accounts.User + alias Lightning.AiAssistant.ChatSession alias Lightning.Projects.Project alias Lightning.Repo alias Lightning.Workflows @@ -37,6 +40,17 @@ defmodule Lightning.Jobs do jobs_for_project_query(project) |> Repo.all() end + def filter_with_chat_user(jobs_ids, %User{id: user_id}) do + from(j in Job, + inner_join: cs in ChatSession, + on: cs.job_id == j.id, + where: + j.id in ^jobs_ids and + cs.user_id == ^user_id and not cs.is_deleted + ) + |> Repo.all() + end + @doc """ Returns the list of jobs excluding the one given. """ diff --git a/lib/lightning/projects.ex b/lib/lightning/projects.ex index 5d25ef82d0..9d20b78c69 100644 --- a/lib/lightning/projects.ex +++ b/lib/lightning/projects.ex @@ -69,7 +69,7 @@ defmodule Lightning.Projects do role: fragment("'support' as role"), workflows_count: count(w.id, :distinct), collaborators_count: count(pu_all.user_id, :distinct), - last_updated_at: max(w.updated_at), + last_updated_at: max(w.updated_at) } ) |> Repo.all() @@ -103,7 +103,9 @@ defmodule Lightning.Projects do on: cs.job_id == j.id, join: w in Workflow, on: j.workflow_id == w.id, - where: cs.user_id == ^user_id and w.project_id == parent_as(:project).id and is_nil(w.deleted_at), + where: + cs.user_id == ^user_id and w.project_id == parent_as(:project).id and + is_nil(w.deleted_at), select: 1 from(p in Project, diff --git a/lib/lightning/workflows.ex b/lib/lightning/workflows.ex index 3df2ba0c11..9a52f65843 100644 --- a/lib/lightning/workflows.ex +++ b/lib/lightning/workflows.ex @@ -10,7 +10,7 @@ defmodule Lightning.Workflows do alias Lightning.KafkaTriggers alias Lightning.Projects.Project alias Lightning.Repo - alias Lightning.Accounts.User + alias Lightning.Accounts.User alias Lightning.AiAssistant.ChatSession alias Lightning.Workflows.Audit alias Lightning.Workflows.Edge @@ -355,7 +355,9 @@ defmodule Lightning.Workflows do on: cs.job_id == j.id, join: w in Workflow, on: j.workflow_id == w.id, - where: cs.user_id == ^user_id and w.project_id == parent_as(:workflow).id and is_nil(w.deleted_at), + where: + cs.user_id == ^user_id and w.project_id == parent_as(:workflow).id and + is_nil(w.deleted_at), select: 1 include = Keyword.get(opts, :include, [:triggers]) diff --git a/lib/lightning_web/live/dashboard_live/components.ex b/lib/lightning_web/live/dashboard_live/components.ex index dd0149a447..f1b7aa6643 100644 --- a/lib/lightning_web/live/dashboard_live/components.ex +++ b/lib/lightning_web/live/dashboard_live/components.ex @@ -114,7 +114,12 @@ defmodule LightningWeb.DashboardLive.Components do onclick={JS.navigate(~p"/projects/#{project.id}/w")} > <.td> - {project.name} <.icon :if={project.has_ai_chat} name="hero-sparkles" class="size-4"/> + {project.name} + <.icon + :if={project.has_ai_chat} + name="hero-sparkles" + class="size-4" + /> <.td class="break-words max-w-[25rem]"> {String.capitalize(to_string(project.role))} diff --git a/lib/lightning_web/live/workflow_live/dashboard_components.ex b/lib/lightning_web/live/workflow_live/dashboard_components.ex index e348c919fe..f84d14a441 100644 --- a/lib/lightning_web/live/workflow_live/dashboard_components.ex +++ b/lib/lightning_web/live/workflow_live/dashboard_components.ex @@ -323,7 +323,8 @@ defmodule LightningWeb.WorkflowLive.DashboardComponents do class="flex-shrink truncate text-gray-900 font-medium workflow-name" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" > - {@workflow.name} <.icon :if={@workflow.has_ai_chat} name="hero-sparkles" class="size-4" /> + {@workflow.name} + <.icon :if={@workflow.has_ai_chat} name="hero-sparkles" class="size-4" /> <%= if @trigger_enabled do %> diff --git a/lib/lightning_web/live/workflow_live/edit.ex b/lib/lightning_web/live/workflow_live/edit.ex index 4f6709cc84..65eae79945 100644 --- a/lib/lightning_web/live/workflow_live/edit.ex +++ b/lib/lightning_web/live/workflow_live/edit.ex @@ -11,6 +11,7 @@ defmodule LightningWeb.WorkflowLive.Edit do alias Lightning.Extensions.UsageLimiting.Action alias Lightning.Extensions.UsageLimiting.Context alias Lightning.Invocation + alias Lightning.Jobs alias Lightning.OauthClients alias Lightning.Policies.Permissions alias Lightning.Policies.ProjectUsers @@ -2834,7 +2835,8 @@ defmodule LightningWeb.WorkflowLive.Edit do socket |> assign( changeset: changeset, - workflow_params: workflow_params + workflow_params: + update_jobs_with_chat_info(workflow_params, socket.assigns.current_user) ) end @@ -3317,4 +3319,22 @@ defmodule LightningWeb.WorkflowLive.Edit do """ end + + defp update_jobs_with_chat_info( + %{"jobs" => jobs} = workflow_params, + current_user + ) do + jobs_with_chat = + jobs + |> Enum.map(& &1["id"]) + |> Jobs.filter_with_chat_user(current_user) + |> MapSet.new() + + Map.update(workflow_params, "jobs", [], fn jobs -> + Enum.map(jobs, fn job -> + has_ai_chat = MapSet.member?(jobs_with_chat, job["id"]) + Map.put(job, "has_ai_chat", has_ai_chat) + end) + end) + end end From 1abd6a6ebc236bef43c478f186c98b0a052d3d3e Mon Sep 17 00:00:00 2001 From: Rogerio Pontual Date: Fri, 30 May 2025 13:26:57 +0200 Subject: [PATCH 03/11] Remove unused code and move update to get-current-state --- lib/lightning_web/live/workflow_live/edit.ex | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/lightning_web/live/workflow_live/edit.ex b/lib/lightning_web/live/workflow_live/edit.ex index 65eae79945..c062044edd 100644 --- a/lib/lightning_web/live/workflow_live/edit.ex +++ b/lib/lightning_web/live/workflow_live/edit.ex @@ -1501,15 +1501,6 @@ defmodule LightningWeb.WorkflowLive.Edit do |> push_patch(to: url) end - @impl true - def handle_event("get-initial-state", _params, socket) do - {:noreply, - socket - |> push_event("current-workflow-params", %{ - workflow_params: socket.assigns.workflow_params - })} - end - @impl true def handle_event("workflow_editor_metrics_report", params, socket) do UiMetrics.log_workflow_editor_metrics( @@ -1521,7 +1512,13 @@ defmodule LightningWeb.WorkflowLive.Edit do end def handle_event("get-current-state", _params, socket) do - {:reply, %{workflow_params: socket.assigns.workflow_params}, socket} + %{workflow_params: workflow_params, current_user: current_user} = + socket.assigns + + {:reply, + %{ + workflow_params: update_jobs_with_chat_info(workflow_params, current_user) + }, socket} end def handle_event( @@ -2835,8 +2832,7 @@ defmodule LightningWeb.WorkflowLive.Edit do socket |> assign( changeset: changeset, - workflow_params: - update_jobs_with_chat_info(workflow_params, socket.assigns.current_user) + workflow_params: workflow_params ) end From df7bf7e179c1fcbd02eb9bbf99f68e8a4f271efb Mon Sep 17 00:00:00 2001 From: Rogerio Pontual Date: Fri, 30 May 2025 16:53:42 +0200 Subject: [PATCH 04/11] Workflow with has_ai_chat indicator passing on the tests --- lib/lightning/workflows.ex | 9 +- test/lightning/workflows_test.exs | 139 ++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 5 deletions(-) diff --git a/lib/lightning/workflows.ex b/lib/lightning/workflows.ex index 9a52f65843..c47c56164d 100644 --- a/lib/lightning/workflows.ex +++ b/lib/lightning/workflows.ex @@ -7,11 +7,11 @@ defmodule Lightning.Workflows do alias Ecto.Multi + alias Lightning.Accounts.User + alias Lightning.AiAssistant.ChatSession alias Lightning.KafkaTriggers alias Lightning.Projects.Project alias Lightning.Repo - alias Lightning.Accounts.User - alias Lightning.AiAssistant.ChatSession alias Lightning.Workflows.Audit alias Lightning.Workflows.Edge alias Lightning.Workflows.Events @@ -355,9 +355,8 @@ defmodule Lightning.Workflows do on: cs.job_id == j.id, join: w in Workflow, on: j.workflow_id == w.id, - where: - cs.user_id == ^user_id and w.project_id == parent_as(:workflow).id and - is_nil(w.deleted_at), + where: cs.user_id == ^user_id, + where: w.project_id == ^project_id and is_nil(w.deleted_at), select: 1 include = Keyword.get(opts, :include, [:triggers]) diff --git a/test/lightning/workflows_test.exs b/test/lightning/workflows_test.exs index 282c15d775..ef65eaea88 100644 --- a/test/lightning/workflows_test.exs +++ b/test/lightning/workflows_test.exs @@ -893,6 +893,145 @@ defmodule Lightning.WorkflowsTest do end end + describe "get_workflows_for/3" do + setup do + project = insert(:project) + + %{user: user} = + insert(:project_user, project: project, user: build(:user)) + + w1 = insert(:simple_workflow, project: project, name: "WorkflowSilent") + w2 = insert(:simple_workflow, project: project, name: "WorkflowChatty1") + w3 = insert(:simple_workflow, project: project, name: "WorkflowChatty2") + + assert w1.project.id == project.id + + Repo.update!(Ecto.Changeset.change(hd(w2.triggers), %{enabled: false})) + + insert(:chat_session, + user: user, + job: hd(w2.jobs), + messages: [ + %{role: :user, content: "what?", user: user} + ] + ) + + insert(:chat_session, + user: user, + job: hd(w3.jobs), + messages: [ + %{role: :user, content: "what not?", user: user} + ] + ) + + %{project: project, w1: w1, w2: w2, w3: w3, user: user} + end + + test "filters workflows by search term", %{project: project, user: user} do + workflows = + Workflows.get_workflows_for(project, user, search: "chatty") + |> Enum.sort_by(& &1.name) + + assert Enum.map(workflows, & &1.name) == [ + "WorkflowChatty1", + "WorkflowChatty2" + ] + + assert Enum.map(workflows, & &1.has_ai_chat) == [true, true] + end + + test "returns empty list for non-matching search", %{ + project: project, + user: user + } do + workflows = + Workflows.get_workflows_for(project, user, search: "nonexistent") + + assert workflows == [] + end + + test "sorts workflows by name ascending", %{project: project, user: user} do + workflows = + Workflows.get_workflows_for(project, user, order_by: {:name, :asc}) + + names = Enum.map(workflows, & &1.name) + assert names == ["WorkflowChatty1", "WorkflowChatty2", "WorkflowSilent"] + end + + test "sorts workflows by name descending", %{project: project, user: user} do + workflows = + Workflows.get_workflows_for(project, user, order_by: {:name, :desc}) + + names = Enum.map(workflows, & &1.name) + assert names == ["WorkflowSilent", "WorkflowChatty2", "WorkflowChatty1"] + end + + test "sorts workflows by enabled state ascending", %{ + project: project, + user: user + } do + workflows = + Workflows.get_workflows_for(project, user, order_by: {:enabled, :asc}) + + first_workflow = List.first(workflows) + last_workflow = List.last(workflows) + + assert first_workflow.triggers |> Enum.any?(& &1.enabled) == false + assert last_workflow.triggers |> Enum.any?(& &1.enabled) == true + end + + test "sorts workflows by enabled state descending", %{ + project: project, + user: user + } do + workflows = + Workflows.get_workflows_for(project, user, order_by: {:enabled, :desc}) + + first_workflow = List.first(workflows) + last_workflow = List.last(workflows) + + assert first_workflow.triggers |> Enum.any?(& &1.enabled) == true + assert last_workflow.triggers |> Enum.any?(& &1.enabled) == false + end + + test "uses default sorting for invalid order_by", %{ + project: project, + user: user + } do + workflows = + Workflows.get_workflows_for(project, user, order_by: {:invalid, :asc}) + + names = Enum.map(workflows, & &1.name) + assert names == ["WorkflowChatty1", "WorkflowChatty2", "WorkflowSilent"] + end + + test "customizes preloaded associations", %{project: project, user: user} do + workflows = + Workflows.get_workflows_for(project, user, include: [:triggers]) + + workflow = List.first(workflows) + + assert workflow.triggers != %Ecto.Association.NotLoaded{} + assert match?(%Ecto.Association.NotLoaded{}, workflow.edges) + end + + test "always includes triggers even if not specified", %{ + project: project, + user: user + } do + workflows = Workflows.get_workflows_for(project, user, include: [:edges]) + workflow = List.first(workflows) + + assert workflow.triggers != %Ecto.Association.NotLoaded{} + assert workflow.edges != %Ecto.Association.NotLoaded{} + end + + test "ignores empty search term", %{project: project, user: user} do + workflows = Workflows.get_workflows_for(project, user, search: "") + assert length(workflows) == 3 + end + end + defp assert_trigger_state_audit( workflow_id, user_id, From 1b45f19f96fcfa929577155ef5e6eb8a356d0641 Mon Sep 17 00:00:00 2001 From: Farhan Yahaya Date: Fri, 30 May 2025 17:00:02 +0000 Subject: [PATCH 05/11] feat: add sparkle to nodes with ai_chat --- assets/js/workflow-diagram/nodes/Node.tsx | 28 +++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/assets/js/workflow-diagram/nodes/Node.tsx b/assets/js/workflow-diagram/nodes/Node.tsx index a8afa5549a..a8ed8e82be 100644 --- a/assets/js/workflow-diagram/nodes/Node.tsx +++ b/assets/js/workflow-diagram/nodes/Node.tsx @@ -4,6 +4,7 @@ import { Handle, type NodeProps } from '@xyflow/react'; import Shape from '../components/Shape'; import ErrorMessage from '../components/ErrorMessage'; import { nodeIconStyles, nodeLabelStyles } from '../styles'; +import { SparklesIcon } from '@heroicons/react/24/outline'; type NodeData = any; @@ -23,6 +24,7 @@ type ErrorObject = { type LabelProps = React.PropsWithChildren<{ hasErrors?: boolean; + hasAi?: boolean; }>; function errorsMessage(errors: ErrorObject): string { @@ -39,16 +41,19 @@ const hasErrors = (errors: ErrorObject | null | undefined): boolean => { return Object.values(errors).some(errorArray => errorArray.length > 0); }; -const Label: React.FC = ({ children, hasErrors = false }) => { +const Label: React.FC = ({ children, hasErrors = false, hasAi = false }) => { const textColorClass = hasErrors ? 'text-red-500' : ''; if (children && (children as any).length) { return ( -

- {children} -

+
+

+ {children} +

+ {hasAi ? : null} +
); } return null; @@ -225,8 +230,8 @@ const Node = ({ }} /> )} -
- +
+ {sublabel} {data.isActiveDropTarget && typeof data.dropTargetError === 'string' && ( @@ -246,10 +251,9 @@ const Node = ({ justifyContent: 'center', }} className={`flex flex-row items-center - opacity-0 ${ - (!data.isActiveDropTarget && 'group-hover:opacity-100') ?? - '' - } + opacity-0 ${(!data.isActiveDropTarget && 'group-hover:opacity-100') ?? + '' + } transition duration-150 ease-in-out`} > {toolbar()} From c55041e5855b651725c817a917e9c1c4df737fd4 Mon Sep 17 00:00:00 2001 From: Rogerio Pontual Date: Mon, 2 Jun 2025 10:05:11 +0200 Subject: [PATCH 06/11] Add test case and fix for Jobs indicator --- lib/lightning_web/live/workflow_live/edit.ex | 2 +- .../live/workflow_live/edit_test.exs | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/lightning_web/live/workflow_live/edit.ex b/lib/lightning_web/live/workflow_live/edit.ex index c062044edd..a8660cff00 100644 --- a/lib/lightning_web/live/workflow_live/edit.ex +++ b/lib/lightning_web/live/workflow_live/edit.ex @@ -3324,7 +3324,7 @@ defmodule LightningWeb.WorkflowLive.Edit do jobs |> Enum.map(& &1["id"]) |> Jobs.filter_with_chat_user(current_user) - |> MapSet.new() + |> MapSet.new(& &1.id) Map.update(workflow_params, "jobs", [], fn jobs -> Enum.map(jobs, fn job -> diff --git a/test/lightning_web/live/workflow_live/edit_test.exs b/test/lightning_web/live/workflow_live/edit_test.exs index 46600c1add..19b4fedc32 100644 --- a/test/lightning_web/live/workflow_live/edit_test.exs +++ b/test/lightning_web/live/workflow_live/edit_test.exs @@ -2393,6 +2393,31 @@ defmodule LightningWeb.WorkflowLive.EditTest do # manual run form body is cleared refute view |> element("#manual_run_form") |> render() =~ body_part end + + test "Indicates a job has AI chat", %{ + conn: conn, + project: project, + workflow: workflow, + user: user + } do + [job1 | _jobs] = workflow.jobs + + insert(:chat_session, + user: user, + job: dbg(job1), + messages: [%{role: :user, content: "what?", user: user}] + ) + + {:ok, view, _html} = + live(conn, ~p"/projects/#{project.id}/w/#{workflow.id}") + + render_hook(view, "get-current-state", %{}) + + assert_reply(view, %{workflow_params: %{"jobs" => reply_jobs}}) + + assert Enum.find(reply_jobs, & &1["id"] == job1.id and &1["has_ai_chat"]) + refute Enum.find(reply_jobs, & &1["id"] != job1.id and &1["has_ai_chat"]) + end end describe "Tracking Workflow editor metrics" do From a80ad5963d220b14ae10718f3d2bdf488b41cf5b Mon Sep 17 00:00:00 2001 From: Rogerio Pontual Date: Mon, 2 Jun 2025 10:11:04 +0200 Subject: [PATCH 07/11] Formatting --- test/lightning_web/live/workflow_live/edit_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/lightning_web/live/workflow_live/edit_test.exs b/test/lightning_web/live/workflow_live/edit_test.exs index 19b4fedc32..006a6f99a2 100644 --- a/test/lightning_web/live/workflow_live/edit_test.exs +++ b/test/lightning_web/live/workflow_live/edit_test.exs @@ -2415,8 +2415,8 @@ defmodule LightningWeb.WorkflowLive.EditTest do assert_reply(view, %{workflow_params: %{"jobs" => reply_jobs}}) - assert Enum.find(reply_jobs, & &1["id"] == job1.id and &1["has_ai_chat"]) - refute Enum.find(reply_jobs, & &1["id"] != job1.id and &1["has_ai_chat"]) + assert Enum.find(reply_jobs, &(&1["id"] == job1.id and &1["has_ai_chat"])) + refute Enum.find(reply_jobs, &(&1["id"] != job1.id and &1["has_ai_chat"])) end end From bd31989b91b59e09055ea25b2a2f30ba2871ded1 Mon Sep 17 00:00:00 2001 From: "Elias W. BA" Date: Thu, 3 Jul 2025 15:42:05 +0000 Subject: [PATCH 08/11] Move icons to the left of project and workflow name --- CHANGELOG.md | 4 ++++ lib/lightning_web/live/dashboard_live/components.ex | 3 ++- lib/lightning_web/live/workflow_live/dashboard_components.ex | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc1a0ffb34..d37a8a1313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -193,6 +193,10 @@ This bug was introduced in version `v2.12.3-pre` on May 29th. If you're tracking ### Changed +- Optimize run claim query performance with per-workflow limiting strategy + [#3245](https://github.com/OpenFn/lightning/pull/3245) +- Add configurable per-workflow claim limit (default: 50) via + `PER_WORKFLOW_CLAIM_LIMIT` environment variable - Update Elixir to 1.18.3 [#2748](https://github.com/OpenFn/lightning/pull/2748) - Standardized table components across the application [#2905](https://github.com/OpenFn/lightning/issues/2905) diff --git a/lib/lightning_web/live/dashboard_live/components.ex b/lib/lightning_web/live/dashboard_live/components.ex index f1b7aa6643..c72c1351ed 100644 --- a/lib/lightning_web/live/dashboard_live/components.ex +++ b/lib/lightning_web/live/dashboard_live/components.ex @@ -113,13 +113,14 @@ defmodule LightningWeb.DashboardLive.Components do class="hover:bg-gray-100 transition-colors duration-200" onclick={JS.navigate(~p"/projects/#{project.id}/w")} > + <% dbg(project) %> <.td> - {project.name} <.icon :if={project.has_ai_chat} name="hero-sparkles" class="size-4" /> + {project.name} <.td class="break-words max-w-[25rem]"> {String.capitalize(to_string(project.role))} diff --git a/lib/lightning_web/live/workflow_live/dashboard_components.ex b/lib/lightning_web/live/workflow_live/dashboard_components.ex index f84d14a441..a563bf5263 100644 --- a/lib/lightning_web/live/workflow_live/dashboard_components.ex +++ b/lib/lightning_web/live/workflow_live/dashboard_components.ex @@ -323,8 +323,8 @@ defmodule LightningWeb.WorkflowLive.DashboardComponents do class="flex-shrink truncate text-gray-900 font-medium workflow-name" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" > - {@workflow.name} <.icon :if={@workflow.has_ai_chat} name="hero-sparkles" class="size-4" /> + {@workflow.name}
<%= if @trigger_enabled do %> From cc00542e98f7d9b4bcd2ae3cde9708cf9e76144d Mon Sep 17 00:00:00 2001 From: "Elias W. BA" Date: Thu, 3 Jul 2025 17:20:42 +0000 Subject: [PATCH 09/11] Include workflow chat ai sessions and fix layout --- lib/lightning/projects.ex | 18 ++++++++++--- lib/lightning/workflows.ex | 25 ++++++++++++++----- .../live/dashboard_live/components.ex | 23 +++++++++-------- .../workflow_live/dashboard_components.ex | 13 +++++----- 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/lib/lightning/projects.ex b/lib/lightning/projects.ex index 9d20b78c69..6acbc4b9a4 100644 --- a/lib/lightning/projects.ex +++ b/lib/lightning/projects.ex @@ -97,17 +97,27 @@ defmodule Lightning.Projects do end defp projects_overview_query(user_id) do - chat_session_exists_query = + job_code_sessions_query = from cs in ChatSession, join: j in Job, on: cs.job_id == j.id, join: w in Workflow, on: j.workflow_id == w.id, where: - cs.user_id == ^user_id and w.project_id == parent_as(:project).id and + cs.user_id == ^user_id and + w.project_id == parent_as(:project).id and + cs.session_type == "job_code" and is_nil(w.deleted_at), select: 1 + workflow_template_sessions_query = + from cs in ChatSession, + where: + cs.user_id == ^user_id and + cs.project_id == parent_as(:project).id and + cs.session_type == "workflow_template", + select: 1 + from(p in Project, as: :project, left_join: w in assoc(p, :workflows), @@ -122,7 +132,9 @@ defmodule Lightning.Projects do workflows_count: count(w.id, :distinct), collaborators_count: count(pu_all.user_id, :distinct), last_updated_at: max(w.updated_at), - has_ai_chat: exists(chat_session_exists_query) + has_ai_chat: + exists(job_code_sessions_query) or + exists(workflow_template_sessions_query) } ) end diff --git a/lib/lightning/workflows.ex b/lib/lightning/workflows.ex index c47c56164d..364046bc85 100644 --- a/lib/lightning/workflows.ex +++ b/lib/lightning/workflows.ex @@ -349,14 +349,22 @@ defmodule Lightning.Workflows do end def get_workflows_for(%Project{id: project_id}, %User{id: user_id}, opts) do - chat_session_exists_query = + job_code_sessions_query = from cs in ChatSession, join: j in Job, on: cs.job_id == j.id, - join: w in Workflow, - on: j.workflow_id == w.id, - where: cs.user_id == ^user_id, - where: w.project_id == ^project_id and is_nil(w.deleted_at), + where: + cs.user_id == ^user_id and + cs.session_type == "job_code" and + j.workflow_id == parent_as(:workflow).id, + select: 1 + + workflow_template_sessions_query = + from cs in ChatSession, + where: + cs.user_id == ^user_id and + cs.session_type == "workflow_template" and + cs.workflow_id == parent_as(:workflow).id, select: 1 include = Keyword.get(opts, :include, [:triggers]) @@ -366,7 +374,12 @@ defmodule Lightning.Workflows do from(w in Workflow, as: :workflow, where: w.project_id == ^project_id and is_nil(w.deleted_at), - select: %{w | has_ai_chat: exists(chat_session_exists_query)}, + select: %{ + w + | has_ai_chat: + exists(job_code_sessions_query) or + exists(workflow_template_sessions_query) + }, preload: ^include ) diff --git a/lib/lightning_web/live/dashboard_live/components.ex b/lib/lightning_web/live/dashboard_live/components.ex index c72c1351ed..5acbbdfe38 100644 --- a/lib/lightning_web/live/dashboard_live/components.ex +++ b/lib/lightning_web/live/dashboard_live/components.ex @@ -79,7 +79,7 @@ defmodule LightningWeb.DashboardLive.Components do
- <.table id="projects-table"> + <.table id="projects-table" class="table-fixed"> <:header> <.tr> <.th @@ -113,22 +113,23 @@ defmodule LightningWeb.DashboardLive.Components do class="hover:bg-gray-100 transition-colors duration-200" onclick={JS.navigate(~p"/projects/#{project.id}/w")} > - <% dbg(project) %> <.td> - <.icon - :if={project.has_ai_chat} - name="hero-sparkles" - class="size-4" - /> - {project.name} +
+ {project.name} + <.icon + :if={project.has_ai_chat} + name="hero-sparkles" + class="size-4 flex-shrink-0" + /> +
- <.td class="break-words max-w-[25rem]"> + <.td> {String.capitalize(to_string(project.role))} - <.td class="break-words max-w-[10rem]"> + <.td> {project.workflows_count} - <.td class="break-words max-w-[5rem]"> + <.td> <.link class="link" href={~p"/projects/#{project.id}/settings#collaboration"} diff --git a/lib/lightning_web/live/workflow_live/dashboard_components.ex b/lib/lightning_web/live/workflow_live/dashboard_components.ex index a563bf5263..2121220955 100644 --- a/lib/lightning_web/live/workflow_live/dashboard_components.ex +++ b/lib/lightning_web/live/workflow_live/dashboard_components.ex @@ -318,14 +318,15 @@ defmodule LightningWeb.WorkflowLive.DashboardComponents do tooltip={@workflow.name} >
-
- - <.icon :if={@workflow.has_ai_chat} name="hero-sparkles" class="size-4" /> +
+ {@workflow.name} + <.icon + :if={@workflow.has_ai_chat} + name="hero-sparkles" + class="size-4 flex-shrink-0" + />
<%= if @trigger_enabled do %>

From aa510ab0829740ced026c608e00d08c3bb093451 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 15 Jul 2025 14:27:51 +0200 Subject: [PATCH 10/11] Organise imports --- assets/js/job-editor/JobEditorComponent.tsx | 2 +- assets/js/workflow-diagram/nodes/Node.tsx | 34 ++++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/assets/js/job-editor/JobEditorComponent.tsx b/assets/js/job-editor/JobEditorComponent.tsx index e16edabd7f..bf7f1e3fac 100644 --- a/assets/js/job-editor/JobEditorComponent.tsx +++ b/assets/js/job-editor/JobEditorComponent.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { ViewColumnsIcon, ChevronLeftIcon, diff --git a/assets/js/workflow-diagram/nodes/Node.tsx b/assets/js/workflow-diagram/nodes/Node.tsx index a8ed8e82be..d05c1f4360 100644 --- a/assets/js/workflow-diagram/nodes/Node.tsx +++ b/assets/js/workflow-diagram/nodes/Node.tsx @@ -1,10 +1,9 @@ -import React, { memo } from 'react'; +import { SparklesIcon } from '@heroicons/react/24/outline'; import { Handle, type NodeProps } from '@xyflow/react'; - -import Shape from '../components/Shape'; +import React, { memo } from 'react'; import ErrorMessage from '../components/ErrorMessage'; +import Shape from '../components/Shape'; import { nodeIconStyles, nodeLabelStyles } from '../styles'; -import { SparklesIcon } from '@heroicons/react/24/outline'; type NodeData = any; @@ -41,18 +40,28 @@ const hasErrors = (errors: ErrorObject | null | undefined): boolean => { return Object.values(errors).some(errorArray => errorArray.length > 0); }; -const Label: React.FC = ({ children, hasErrors = false, hasAi = false }) => { +const Label: React.FC = ({ + children, + hasErrors = false, + hasAi = false, +}) => { const textColorClass = hasErrors ? 'text-red-500' : ''; if (children && (children as any).length) { return ( -

+

{children}

- {hasAi ? : null} + {hasAi ? ( + + ) : null}
); } @@ -231,7 +240,9 @@ const Node = ({ /> )}
- + {sublabel} {data.isActiveDropTarget && typeof data.dropTargetError === 'string' && ( @@ -251,9 +262,10 @@ const Node = ({ justifyContent: 'center', }} className={`flex flex-row items-center - opacity-0 ${(!data.isActiveDropTarget && 'group-hover:opacity-100') ?? - '' - } + opacity-0 ${ + (!data.isActiveDropTarget && 'group-hover:opacity-100') ?? + '' + } transition duration-150 ease-in-out`} > {toolbar()} From 1187a26eded636d2dc29a890d6dcf544a3c4a6d8 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 15 Jul 2025 14:30:41 +0200 Subject: [PATCH 11/11] Update CHANGELOG [ci skip] --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd3f8fd5a8..351de7c0b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to ### Added +- Indicate chat exists on project, workflow or job + [#2922](https://github.com/OpenFn/lightning/issues/2922) + ### Changed ### Fixed