Skip to content

[feat] Support Multi-Version Workflows #11990

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4e41b8a
feat: support workflow multi version recovery
hobo-l-20230331 Sep 16, 2024
b1397e2
feat: support workflow multi version recovery
hobo-l-20230331 Sep 16, 2024
0c91279
resolve conflict
crazywoola Dec 20, 2024
7004656
resolve conflict
crazywoola Dec 20, 2024
0853f66
resolve conflict
crazywoola Dec 20, 2024
03af025
fix: ui
crazywoola Dec 20, 2024
a20f15d
chore: make linter happy
crazywoola Dec 20, 2024
b7bf30b
Merge remote-tracking branch 'origin-2/main' into feat/multi_version_…
warren830 Dec 22, 2024
73cb902
modify
warren830 Dec 22, 2024
fad1bbb
Merge branch 'langgenius:main' into main
warren830 Dec 23, 2024
82de0ee
fix
warren830 Dec 23, 2024
957fa7f
fix
warren830 Dec 23, 2024
02ed307
fix
warren830 Dec 23, 2024
2599646
Merge branch 'langgenius:main' into main
warren830 Dec 23, 2024
8ee8a21
Merge branch 'main' of github.com:warren830/dify
warren830 Dec 23, 2024
981643b
fix
warren830 Dec 23, 2024
a3b1df5
fix lint
warren830 Dec 23, 2024
52de345
reformat api
warren830 Dec 23, 2024
bf0118a
Merge remote-tracking branch 'remote/main'
warren830 Dec 25, 2024
440141a
fix
warren830 Dec 25, 2024
427fb24
fix
warren830 Dec 26, 2024
24186ae
add created by
warren830 Dec 26, 2024
2fac785
reformat
warren830 Dec 26, 2024
edef4cc
reformat
warren830 Dec 26, 2024
cf6d313
reformat
warren830 Dec 26, 2024
b2bdf36
Merge remote-tracking branch 'remote/main'
warren830 Dec 26, 2024
2f12a8e
redesign restore list
warren830 Dec 26, 2024
276aa6e
fix: style
crazywoola Dec 27, 2024
a2d8b8f
process fields in frontend
warren830 Dec 27, 2024
fd5bbdd
process fields in frontend
warren830 Dec 27, 2024
6b4702d
fix
warren830 Dec 27, 2024
a665ddb
fix
warren830 Dec 27, 2024
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
30 changes: 28 additions & 2 deletions api/controllers/console/app/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging

from flask import abort, request
from flask_restful import Resource, marshal_with, reqparse # type: ignore
from flask_restful import Resource, inputs, marshal_with, reqparse # type: ignore
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound

import services
Expand All @@ -14,7 +14,7 @@
from core.app.apps.base_app_queue_manager import AppQueueManager
from core.app.entities.app_invoke_entities import InvokeFrom
from factories import variable_factory
from fields.workflow_fields import workflow_fields
from fields.workflow_fields import workflow_fields, workflow_pagination_fields
from fields.workflow_run_fields import workflow_run_node_execution_fields
from libs import helper
from libs.helper import TimestampField, uuid_value
Expand Down Expand Up @@ -440,6 +440,31 @@ def get(self, app_model: App):
}


class PublishedAllWorkflowApi(Resource):
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@marshal_with(workflow_pagination_fields)
def get(self, app_model: App):
"""
Get published workflows
"""
if not current_user.is_editor:
raise Forbidden()

parser = reqparse.RequestParser()
parser.add_argument("page", type=inputs.int_range(1, 99999), required=False, default=1, location="args")
parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
args = parser.parse_args()
page = args.get("page")
limit = args.get("limit")
workflow_service = WorkflowService()
workflows, has_more = workflow_service.get_all_published_workflow(app_model=app_model, page=page, limit=limit)

return {"items": workflows, "page": page, "limit": limit, "has_more": has_more}


api.add_resource(DraftWorkflowApi, "/apps/<uuid:app_id>/workflows/draft")
api.add_resource(WorkflowConfigApi, "/apps/<uuid:app_id>/workflows/draft/config")
api.add_resource(AdvancedChatDraftWorkflowRunApi, "/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
Expand All @@ -454,6 +479,7 @@ def get(self, app_model: App):
WorkflowDraftRunIterationNodeApi, "/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run"
)
api.add_resource(PublishedWorkflowApi, "/apps/<uuid:app_id>/workflows/publish")
api.add_resource(PublishedAllWorkflowApi, "/apps/<uuid:app_id>/workflows")
api.add_resource(DefaultBlockConfigsApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
api.add_resource(
DefaultBlockConfigApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>"
Expand Down
8 changes: 8 additions & 0 deletions api/fields/workflow_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def format(self, value):
"graph": fields.Raw(attribute="graph_dict"),
"features": fields.Raw(attribute="features_dict"),
"hash": fields.String(attribute="unique_hash"),
"version": fields.String(attribute="version"),
"created_by": fields.Nested(simple_account_fields, attribute="created_by_account"),
"created_at": TimestampField,
"updated_by": fields.Nested(simple_account_fields, attribute="updated_by_account", allow_null=True),
Expand All @@ -61,3 +62,10 @@ def format(self, value):
"updated_by": fields.String,
"updated_at": TimestampField,
}

workflow_pagination_fields = {
"items": fields.List(fields.Nested(workflow_fields), attribute="items"),
"page": fields.Integer,
"limit": fields.Integer(attribute="limit"),
"has_more": fields.Boolean(attribute="has_more"),
}
24 changes: 24 additions & 0 deletions api/services/workflow_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from typing import Any, Optional, cast
from uuid import uuid4

from sqlalchemy import desc

from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager
from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager
from core.model_runtime.utils.encoders import jsonable_encoder
Expand Down Expand Up @@ -76,6 +78,28 @@ def get_published_workflow(self, app_model: App) -> Optional[Workflow]:

return workflow

def get_all_published_workflow(self, app_model: App, page: int, limit: int) -> tuple[list[Workflow], bool]:
"""
Get published workflow with pagination
"""
if not app_model.workflow_id:
return [], False

workflows = (
db.session.query(Workflow)
.filter(Workflow.app_id == app_model.id)
.order_by(desc(Workflow.version))
.offset((page - 1) * limit)
.limit(limit + 1)
.all()
)

has_more = len(workflows) > limit
if has_more:
workflows = workflows[:-1]

return workflows, has_more

def sync_draft_workflow(
self,
*,
Expand Down
49 changes: 29 additions & 20 deletions web/app/components/workflow/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { StartNodeType } from '../nodes/start/types'
import {
useChecklistBeforePublish,
useIsChatMode,
useNodesInteractions,
useNodesReadOnly,
useNodesSyncDraft,
useWorkflowMode,
Expand All @@ -35,6 +36,7 @@ import RestoringTitle from './restoring-title'
import ViewHistory from './view-history'
import ChatVariableButton from './chat-variable-button'
import EnvButton from './env-button'
import VersionHistoryModal from './version-history-modal'
import Button from '@/app/components/base/button'
import { useStore as useAppStore } from '@/app/components/app/store'
import { publishWorkflow } from '@/service/workflow'
Expand All @@ -49,11 +51,13 @@ const Header: FC = () => {
const appID = appDetail?.id
const isChatMode = useIsChatMode()
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
const { handleNodeSelect } = useNodesInteractions()
const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const toolPublished = useStore(s => s.toolPublished)
const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const selectedNode = nodes.find(node => node.data.selected)
const startVariables = startNode?.data.variables
const fileSettings = useFeatures(s => s.features.file)
const variables = useMemo(() => {
Expand All @@ -76,7 +80,6 @@ const Header: FC = () => {
const {
handleLoadBackupDraft,
handleBackupDraft,
handleRestoreFromPublishedWorkflow,
} = useWorkflowRun()
const { handleCheckBeforePublish } = useChecklistBeforePublish()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
Expand Down Expand Up @@ -126,8 +129,10 @@ const Header: FC = () => {
const onStartRestoring = useCallback(() => {
workflowStore.setState({ isRestoring: true })
handleBackupDraft()
handleRestoreFromPublishedWorkflow()
}, [handleBackupDraft, handleRestoreFromPublishedWorkflow, workflowStore])
// clear right panel
if (selectedNode)
handleNodeSelect(selectedNode.id, true)
}, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode])

const onPublisherToggle = useCallback((state: boolean) => {
if (state)
Expand Down Expand Up @@ -209,23 +214,27 @@ const Header: FC = () => {
}
{
restoring && (
<div className='flex items-center space-x-2'>
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
<RiApps2AddLine className='w-4 h-4 mr-1 text-components-button-secondary-text' />
{t('workflow.common.features')}
</Button>
<Divider type='vertical' className='h-3.5 mx-auto' />
<Button
onClick={handleCancelRestore}
>
{t('common.operation.cancel')}
</Button>
<Button
onClick={handleRestore}
variant='primary'
>
{t('workflow.common.restore')}
</Button>
<div className='flex flex-col mt-auto'>
<div className='flex items-center justify-end my-4'>
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
<RiApps2AddLine className='w-4 h-4 mr-1 text-components-button-secondary-text' />
{t('workflow.common.features')}
</Button>
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Button
className='mr-2'
onClick={handleCancelRestore}
>
{t('common.operation.cancel')}
</Button>
<Button
onClick={handleRestore}
variant='primary'
>
{t('workflow.common.restore')}
</Button>
</div>
<VersionHistoryModal />
</div>
)
}
Expand Down
66 changes: 66 additions & 0 deletions web/app/components/workflow/header/version-history-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import { WorkflowVersion } from '../types'
import cn from '@/utils/classnames'
import type { VersionHistory } from '@/types/workflow'

type VersionHistoryItemProps = {
item: VersionHistory
selectedVersion: string
onClick: (item: VersionHistory) => void
curIdx: number
page: number
}

const formatVersion = (version: string, curIdx: number, page: number): string => {
if (curIdx === 0 && page === 1)
return WorkflowVersion.Draft
if (curIdx === 1 && page === 1)
return WorkflowVersion.Latest
try {
const date = new Date(version)
if (isNaN(date.getTime()))
return version

// format as YYYY-MM-DD HH:mm:ss
return date.toISOString().slice(0, 19).replace('T', ' ')
}
catch {
return version
}
}

const VersionHistoryItem: React.FC<VersionHistoryItemProps> = ({ item, selectedVersion, onClick, curIdx, page }) => {
const { t } = useTranslation()
const formatTime = (time: number) => dayjs.unix(time).format('YYYY-MM-DD HH:mm:ss')
const formattedVersion = formatVersion(item.version, curIdx, page)
const renderVersionLabel = (version: string) => (
(version === WorkflowVersion.Draft || version === WorkflowVersion.Latest)
? (
<div className="shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate">
{version}
</div>
)
: null
)

return (
<div
className={cn(
'flex items-center p-2 h-12 text-xs font-medium text-gray-700 justify-between',
formattedVersion === selectedVersion ? '' : 'hover:bg-gray-100',
formattedVersion === WorkflowVersion.Draft ? 'cursor-not-allowed' : 'cursor-pointer',
)}
onClick={() => item.version !== WorkflowVersion.Draft && onClick(item)}
>
<div className='flex flex-col gap-1 py-2'>
<span className="text-left">{formatTime(formattedVersion === WorkflowVersion.Draft ? item.updated_at : item.created_at)}</span>
<span className="text-left">{t('workflow.panel.createdBy')} {item.created_by.name}</span>
</div>
{renderVersionLabel(formattedVersion)}
</div>
)
}

export default React.memo(VersionHistoryItem)
89 changes: 89 additions & 0 deletions web/app/components/workflow/header/version-history-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useWorkflowRun } from '../hooks'
import VersionHistoryItem from './version-history-item'
import type { VersionHistory } from '@/types/workflow'
import { useStore as useAppStore } from '@/app/components/app/store'
import { fetchPublishedAllWorkflow } from '@/service/workflow'
import Loading from '@/app/components/base/loading'
import Button from '@/app/components/base/button'

const limit = 10

const VersionHistoryModal = () => {
const [selectedVersion, setSelectedVersion] = useState('draft')
const [page, setPage] = useState(1)
const { handleRestoreFromPublishedWorkflow } = useWorkflowRun()
const appDetail = useAppStore.getState().appDetail
const { t } = useTranslation()
const {
data: versionHistory,
isLoading,
} = useSWR(
`/apps/${appDetail?.id}/workflows?page=${page}&limit=${limit}`,
fetchPublishedAllWorkflow,
)

const handleVersionClick = (item: VersionHistory) => {
if (item.version !== selectedVersion) {
setSelectedVersion(item.version)
handleRestoreFromPublishedWorkflow(item)
}
}

const handleNextPage = () => {
if (versionHistory?.has_more)
setPage(page => page + 1)
}

return (
<div className='w-[336px] bg-white rounded-2xl border-[0.5px] border-gray-200 shadow-xl p-2'>
<div className="max-h-[400px] overflow-auto">
{(isLoading && page) === 1
? (
<div className='flex items-center justify-center h-10'>
<Loading/>
</div>
)
: (
<>
{versionHistory?.items?.map((item, idx) => (
<VersionHistoryItem
key={item.version}
item={item}
selectedVersion={selectedVersion}
onClick={handleVersionClick}
curIdx={idx}
page={page}
/>
))}
{isLoading && page > 1 && (
<div className='flex items-center justify-center h-10'>
<Loading/>
</div>
)}
{!isLoading && versionHistory?.has_more && (
<div className='flex items-center justify-center h-10 mt-2'>
<Button
className='text-sm'
onClick={handleNextPage}
>
{t('workflow.common.loadMore')}
</Button>
</div>
)}
{!isLoading && !versionHistory?.items?.length && (
<div className='flex items-center justify-center h-10 text-gray-500'>
{t('workflow.common.noHistory')}
</div>
)}
</>
)}
</div>
</div>
)
}

export default React.memo(VersionHistoryModal)
2 changes: 1 addition & 1 deletion web/app/components/workflow/header/view-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ const ViewHistory = ({
{`Test ${isChatMode ? 'Chat' : 'Run'}#${item.sequence_number}`}
</div>
<div className='flex items-center text-xs text-gray-500 leading-[18px]'>
{item.created_by_account.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)}
{item.created_by_account?.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)}
</div>
</div>
</div>
Expand Down
Loading
Loading