Skip to content
Open
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -240,6 +243,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)
Expand Down
2 changes: 1 addition & 1 deletion assets/js/job-editor/JobEditorComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
ViewColumnsIcon,
ChevronLeftIcon,
Expand Down
38 changes: 27 additions & 11 deletions assets/js/workflow-diagram/nodes/Node.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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';

type NodeData = any;
Expand All @@ -23,6 +23,7 @@ type ErrorObject = {

type LabelProps = React.PropsWithChildren<{
hasErrors?: boolean;
hasAi?: boolean;
}>;

function errorsMessage(errors: ErrorObject): string {
Expand All @@ -39,16 +40,29 @@ const hasErrors = (errors: ErrorObject | null | undefined): boolean => {
return Object.values(errors).some(errorArray => errorArray.length > 0);
};

const Label: React.FC<LabelProps> = ({ children, hasErrors = false }) => {
const Label: React.FC<LabelProps> = ({
children,
hasErrors = false,
hasAi = false,
}) => {
const textColorClass = hasErrors ? 'text-red-500' : '';

if (children && (children as any).length) {
return (
<p
className={`line-clamp-2 align-left text-m max-w-[120px] text-ellipsis overflow-hidden ${textColorClass}`}
>
{children}
</p>
<div className="inline-flex">
<p
className={`flex line-clamp-2 align-left text-m max-w-[275px] text-ellipsis overflow-hidden ${textColorClass}`}
>
{children}
</p>
{hasAi ? (
<SparklesIcon
title="AI chat was used in this job"
className="w-5 h-5 ml-1"
color="#FF5722"
/>
) : null}
</div>
);
}
return null;
Expand Down Expand Up @@ -225,8 +239,10 @@ const Node = ({
}}
/>
)}
<div className="flex flex-col mt-8 ml-2 absolute left-[116px] top-0 pointer-events-none min-w-[275px]">
<Label hasErrors={hasErrors(errors)}>{label}</Label>
<div className="flex flex-col flex-1 ml-2 mt-8">
<Label hasErrors={hasErrors(errors)} hasAi={!!data.has_ai_chat}>
{label}{' '}
</Label>
<SubLabel>{sublabel}</SubLabel>
{data.isActiveDropTarget &&
typeof data.dropTargetError === 'string' && (
Expand Down
14 changes: 14 additions & 0 deletions lib/lightning/jobs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
"""
Expand Down
31 changes: 29 additions & 2 deletions lib/lightning/projects.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -46,7 +47,8 @@ defmodule Lightning.Projects do
:role,
:workflows_count,
:collaborators_count,
:last_updated_at
:last_updated_at,
:has_ai_chat
]
end

Expand Down Expand Up @@ -95,7 +97,29 @@ defmodule Lightning.Projects do
end

defp projects_overview_query(user_id) do
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.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),
inner_join: pu in assoc(p, :project_users),
left_join: pu_all in assoc(p, :project_users),
Expand All @@ -107,7 +131,10 @@ 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(job_code_sessions_query) or
exists(workflow_template_sessions_query)
}
)
end
Expand Down
57 changes: 51 additions & 6 deletions lib/lightning/workflows.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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
Expand Down Expand Up @@ -336,22 +338,65 @@ 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
job_code_sessions_query =
from cs in ChatSession,
join: j in Job,
on: cs.job_id == j.id,
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])
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(job_code_sessions_query) or
exists(workflow_template_sessions_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
Expand Down
1 change: 1 addition & 0 deletions lib/lightning/workflows/workflow.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions lib/lightning_web/live/dashboard_live/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ defmodule LightningWeb.DashboardLive.Components do
</div>
</div>
<div>
<.table id="projects-table">
<.table id="projects-table" class="table-fixed">
<:header>
<.tr>
<.th
Expand Down Expand Up @@ -114,7 +114,14 @@ defmodule LightningWeb.DashboardLive.Components do
onclick={JS.navigate(~p"/projects/#{project.id}/w")}
>
<.td>
{project.name}
<div class="flex items-center gap-1 max-w-[15rem]">
<span class="truncate">{project.name}</span>
<.icon
:if={project.has_ai_chat}
name="hero-sparkles"
class="size-4 flex-shrink-0"
/>
</div>
</.td>
<.td class="wrap-break-word max-w-[25rem]">
{String.capitalize(to_string(project.role))}
Expand Down
12 changes: 7 additions & 5 deletions lib/lightning_web/live/workflow_live/dashboard_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -318,13 +318,15 @@ defmodule LightningWeb.WorkflowLive.DashboardComponents do
tooltip={@workflow.name}
>
<div class="text-sm">
<div class="flex items-center">
<span
class="flex-shrink truncate text-gray-900 font-medium workflow-name"
style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
>
<div class="flex items-center gap-1 max-w-[15rem]">
<span class="truncate text-gray-900 font-medium workflow-name">
{@workflow.name}
</span>
<.icon
:if={@workflow.has_ai_chat}
name="hero-sparkles"
class="size-4 flex-shrink-0"
/>
</div>
<%= if @trigger_enabled do %>
<p class="text-gray-500 text-xs mt-1">
Expand Down
36 changes: 26 additions & 10 deletions lib/lightning_web/live/workflow_live/edit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1500,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(
Expand All @@ -1520,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(
Expand Down Expand Up @@ -3335,4 +3333,22 @@ defmodule LightningWeb.WorkflowLive.Edit do
</div>
"""
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(& &1.id)

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
3 changes: 2 additions & 1 deletion lib/lightning_web/live/workflow_live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 =
Expand Down
Loading