Browse Source

Export DSL from history (#24939)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
tags/1.8.1
GuanMu 2 months ago
parent
commit
25a11bfafc
No account linked to committer's email address

+ 6
- 1
api/controllers/console/app/app.py View File

# Add include_secret params # Add include_secret params
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("include_secret", type=inputs.boolean, default=False, location="args") parser.add_argument("include_secret", type=inputs.boolean, default=False, location="args")
parser.add_argument("workflow_id", type=str, location="args")
args = parser.parse_args() args = parser.parse_args()


return {"data": AppDslService.export_dsl(app_model=app_model, include_secret=args["include_secret"])}
return {
"data": AppDslService.export_dsl(
app_model=app_model, include_secret=args["include_secret"], workflow_id=args.get("workflow_id")
)
}




class AppNameApi(Resource): class AppNameApi(Resource):

+ 6
- 4
api/services/app_dsl_service.py View File

return app return app


@classmethod @classmethod
def export_dsl(cls, app_model: App, include_secret: bool = False) -> str:
def export_dsl(cls, app_model: App, include_secret: bool = False, workflow_id: Optional[str] = None) -> str:
""" """
Export app Export app
:param app_model: App instance :param app_model: App instance


if app_mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}: if app_mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
cls._append_workflow_export_data( cls._append_workflow_export_data(
export_data=export_data, app_model=app_model, include_secret=include_secret
export_data=export_data, app_model=app_model, include_secret=include_secret, workflow_id=workflow_id
) )
else: else:
cls._append_model_config_export_data(export_data, app_model) cls._append_model_config_export_data(export_data, app_model)
return yaml.dump(export_data, allow_unicode=True) # type: ignore return yaml.dump(export_data, allow_unicode=True) # type: ignore


@classmethod @classmethod
def _append_workflow_export_data(cls, *, export_data: dict, app_model: App, include_secret: bool) -> None:
def _append_workflow_export_data(
cls, *, export_data: dict, app_model: App, include_secret: bool, workflow_id: Optional[str] = None
) -> None:
""" """
Append workflow export data Append workflow export data
:param export_data: export data :param export_data: export data
:param app_model: App instance :param app_model: App instance
""" """
workflow_service = WorkflowService() workflow_service = WorkflowService()
workflow = workflow_service.get_draft_workflow(app_model)
workflow = workflow_service.get_draft_workflow(app_model, workflow_id)
if not workflow: if not workflow:
raise ValueError("Missing draft workflow configuration, please check.") raise ValueError("Missing draft workflow configuration, please check.")



+ 6
- 2
api/services/workflow_service.py View File

) )
return db.session.execute(stmt).scalar_one() return db.session.execute(stmt).scalar_one()


def get_draft_workflow(self, app_model: App) -> Optional[Workflow]:
def get_draft_workflow(self, app_model: App, workflow_id: Optional[str] = None) -> Optional[Workflow]:
""" """
Get draft workflow Get draft workflow
""" """
if workflow_id:
return self.get_published_workflow_by_id(app_model, workflow_id)
# fetch draft workflow by app_model # fetch draft workflow by app_model
workflow = ( workflow = (
db.session.query(Workflow) db.session.query(Workflow)
return workflow return workflow


def get_published_workflow_by_id(self, app_model: App, workflow_id: str) -> Optional[Workflow]: def get_published_workflow_by_id(self, app_model: App, workflow_id: str) -> Optional[Workflow]:
# fetch published workflow by workflow_id
"""
fetch published workflow by workflow_id
"""
workflow = ( workflow = (
db.session.query(Workflow) db.session.query(Workflow)
.where( .where(

+ 81
- 1
api/tests/test_containers_integration_tests/services/test_app_dsl_service.py View File



# Verify workflow service was called # Verify workflow service was called
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with( mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with(
app
app, None
)

def test_export_dsl_with_workflow_id_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful DSL export with specific workflow ID.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)

# Update app to workflow mode
app.mode = "workflow"
db_session_with_containers.commit()

# Mock workflow service to return a workflow when specific workflow_id is provided
mock_workflow = MagicMock()
mock_workflow.to_dict.return_value = {
"graph": {"nodes": [{"id": "start", "type": "start", "data": {"type": "start"}}], "edges": []},
"features": {},
"environment_variables": [],
"conversation_variables": [],
}

# Mock the get_draft_workflow method to return different workflows based on workflow_id
def mock_get_draft_workflow(app_model, workflow_id=None):
if workflow_id == "specific-workflow-id":
return mock_workflow
return None

mock_external_service_dependencies[
"workflow_service"
].return_value.get_draft_workflow.side_effect = mock_get_draft_workflow

# Export DSL with specific workflow ID
exported_dsl = AppDslService.export_dsl(app, include_secret=False, workflow_id="specific-workflow-id")

# Parse exported YAML
exported_data = yaml.safe_load(exported_dsl)

# Verify exported data structure
assert exported_data["kind"] == "app"
assert exported_data["app"]["name"] == app.name
assert exported_data["app"]["mode"] == "workflow"

# Verify workflow was exported
assert "workflow" in exported_data
assert "graph" in exported_data["workflow"]
assert "nodes" in exported_data["workflow"]["graph"]

# Verify dependencies were exported
assert "dependencies" in exported_data
assert isinstance(exported_data["dependencies"], list)

# Verify workflow service was called with specific workflow ID
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with(
app, "specific-workflow-id"
)

def test_export_dsl_with_invalid_workflow_id_raises_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test that export_dsl raises error when invalid workflow ID is provided.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)

# Update app to workflow mode
app.mode = "workflow"
db_session_with_containers.commit()

# Mock workflow service to return None when invalid workflow ID is provided
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.return_value = None

# Export DSL with invalid workflow ID should raise ValueError
with pytest.raises(ValueError, match="Missing draft workflow configuration, please check."):
AppDslService.export_dsl(app, include_secret=False, workflow_id="invalid-workflow-id")

# Verify workflow service was called with the invalid workflow ID
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with(
app, "invalid-workflow-id"
) )


def test_check_dependencies_success(self, db_session_with_containers, mock_external_service_dependencies): def test_check_dependencies_success(self, db_session_with_containers, mock_external_service_dependencies):

+ 2
- 1
web/app/components/workflow/hooks/use-workflow-interactions.ts View File



const appDetail = useAppStore(s => s.appDetail) const appDetail = useAppStore(s => s.appDetail)


const handleExportDSL = useCallback(async (include = false) => {
const handleExportDSL = useCallback(async (include = false, workflowId?: string) => {
if (!appDetail) if (!appDetail)
return return


await doSyncWorkflowDraft() await doSyncWorkflowDraft()
const { data } = await exportAppConfig({ const { data } = await exportAppConfig({
appID: appDetail.id, appID: appDetail.id,
workflowID: workflowId,
include, include,
}) })
const a = document.createElement('a') const a = document.createElement('a')

+ 4
- 0
web/app/components/workflow/panel/version-history-panel/context-menu/use-context-menu.ts View File

key: VersionHistoryContextMenuOptions.edit, key: VersionHistoryContextMenuOptions.edit,
name: t('workflow.versionHistory.nameThisVersion'), name: t('workflow.versionHistory.nameThisVersion'),
}, },
{
key: VersionHistoryContextMenuOptions.exportDSL,
name: t('app.export'),
},
{ {
key: VersionHistoryContextMenuOptions.copyId, key: VersionHistoryContextMenuOptions.copyId,
name: t('workflow.versionHistory.copyId'), name: t('workflow.versionHistory.copyId'),

+ 6
- 2
web/app/components/workflow/panel/version-history-panel/index.tsx View File

import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiArrowDownDoubleLine, RiCloseLine, RiLoader2Line } from '@remixicon/react' import { RiArrowDownDoubleLine, RiCloseLine, RiLoader2Line } from '@remixicon/react'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import { useNodesSyncDraft, useWorkflowRun } from '../../hooks'
import { useDSL, useNodesSyncDraft, useWorkflowRun } from '../../hooks'
import { useStore, useWorkflowStore } from '../../store' import { useStore, useWorkflowStore } from '../../store'
import { VersionHistoryContextMenuOptions, WorkflowVersionFilterOptions } from '../../types' import { VersionHistoryContextMenuOptions, WorkflowVersionFilterOptions } from '../../types'
import VersionHistoryItem from './version-history-item' import VersionHistoryItem from './version-history-item'
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { handleRestoreFromPublishedWorkflow, handleLoadBackupDraft } = useWorkflowRun() const { handleRestoreFromPublishedWorkflow, handleLoadBackupDraft } = useWorkflowRun()
const { handleExportDSL } = useDSL()
const appDetail = useAppStore.getState().appDetail const appDetail = useAppStore.getState().appDetail
const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel) const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel)
const currentVersion = useStore(s => s.currentVersion) const currentVersion = useStore(s => s.currentVersion)
message: t('workflow.versionHistory.action.copyIdSuccess'), message: t('workflow.versionHistory.action.copyIdSuccess'),
}) })
break break
case VersionHistoryContextMenuOptions.exportDSL:
handleExportDSL(false, item.id)
break
} }
}, [t])
}, [t, handleExportDSL])


const handleCancel = useCallback((operation: VersionHistoryContextMenuOptions) => { const handleCancel = useCallback((operation: VersionHistoryContextMenuOptions) => {
switch (operation) { switch (operation) {

+ 1
- 0
web/app/components/workflow/types.ts View File

restore = 'restore', restore = 'restore',
edit = 'edit', edit = 'edit',
delete = 'delete', delete = 'delete',
exportDSL = 'exportDSL',
copyId = 'copyId', copyId = 'copyId',
} }



+ 7
- 2
web/service/apps.ts View File

return post<AppDetailResponse>(`apps/${appID}/copy`, { body: { name, icon_type, icon, icon_background, mode, description } }) return post<AppDetailResponse>(`apps/${appID}/copy`, { body: { name, icon_type, icon, icon_background, mode, description } })
} }


export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include?: boolean }> = ({ appID, include = false }) => {
return get<{ data: string }>(`apps/${appID}/export?include_secret=${include}`)
export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include?: boolean; workflowID?: string }> = ({ appID, include = false, workflowID }) => {
const params = new URLSearchParams({
include_secret: include.toString(),
})
if (workflowID)
params.append('workflow_id', workflowID)
return get<{ data: string }>(`apps/${appID}/export?${params.toString()}`)
} }


// TODO: delete // TODO: delete

Loading…
Cancel
Save