Browse Source

feat: plugin auto upgrade strategy (#19758)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Novice <novice12185727@gmail.com>
tags/1.7.0
Junyan Qin (Chin) 3 months ago
parent
commit
eaae79a581
No account linked to committer's email address
100 changed files with 1094 additions and 322 deletions
  1. 10
    0
      api/.env.example
  2. 6
    1
      api/README.md
  3. 36
    0
      api/configs/feature/__init__.py
  4. 114
    1
      api/controllers/console/workspace/plugin.py
  5. 20
    0
      api/core/helper/marketplace.py
  6. 1
    1
      api/docker/entrypoint.sh
  7. 39
    26
      api/extensions/ext_celery.py
  8. 42
    0
      api/migrations/versions/2025_07_23_1508-8bcc02c9bd07_add_tenant_plugin_autoupgrade_table.py
  9. 37
    3
      api/models/account.py
  10. 49
    0
      api/schedule/check_upgradable_plugin_task.py
  11. 12
    0
      api/services/account_service.py
  12. 87
    0
      api/services/plugin/plugin_auto_upgrade_service.py
  13. 166
    0
      api/tasks/process_tenant_plugin_autoupgrade_check_task.py
  14. 10
    0
      docker/.env.example
  15. 19
    0
      docker/docker-compose-template.yaml
  16. 27
    0
      docker/docker-compose.yaml
  17. 4
    1
      web/app/components/base/date-and-time-picker/common/option-list-item.tsx
  18. 7
    2
      web/app/components/base/date-and-time-picker/time-picker/header.tsx
  19. 21
    10
      web/app/components/base/date-and-time-picker/time-picker/index.tsx
  20. 3
    1
      web/app/components/base/date-and-time-picker/time-picker/options.tsx
  21. 11
    1
      web/app/components/base/date-and-time-picker/types.ts
  22. 12
    0
      web/app/components/base/date-and-time-picker/utils/dayjs.ts
  23. 7
    0
      web/app/components/base/icons/assets/vender/line/general/search-menu.svg
  24. 4
    0
      web/app/components/base/icons/assets/vender/system/auto-update-line.svg
  25. 1
    1
      web/app/components/base/icons/script.mjs
  26. 129
    116
      web/app/components/base/icons/src/public/tracing/AliyunIcon.json
  27. 9
    5
      web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx
  28. 115
    69
      web/app/components/base/icons/src/public/tracing/AliyunIconBig.json
  29. 9
    5
      web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx
  30. 9
    5
      web/app/components/base/icons/src/public/tracing/WeaveIcon.tsx
  31. 9
    5
      web/app/components/base/icons/src/public/tracing/WeaveIconBig.tsx
  32. 2
    2
      web/app/components/base/icons/src/public/tracing/index.ts
  33. 1
    1
      web/app/components/base/icons/src/vender/features/Citations.json
  34. 1
    1
      web/app/components/base/icons/src/vender/features/ContentModeration.json
  35. 1
    1
      web/app/components/base/icons/src/vender/features/Document.json
  36. 1
    1
      web/app/components/base/icons/src/vender/features/FolderUpload.json
  37. 1
    1
      web/app/components/base/icons/src/vender/features/LoveMessage.json
  38. 1
    1
      web/app/components/base/icons/src/vender/features/MessageFast.json
  39. 1
    1
      web/app/components/base/icons/src/vender/features/Microphone01.json
  40. 1
    1
      web/app/components/base/icons/src/vender/features/TextToAudio.json
  41. 1
    1
      web/app/components/base/icons/src/vender/features/VirtualAssistant.json
  42. 1
    1
      web/app/components/base/icons/src/vender/features/Vision.json
  43. 1
    1
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/AlertTriangle.json
  44. 1
    1
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json
  45. 1
    1
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json
  46. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ArrowNarrowLeft.json
  47. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ArrowUpRight.json
  48. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ChevronDownDouble.json
  49. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ChevronRight.json
  50. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.json
  51. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/RefreshCcw01.json
  52. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/RefreshCw05.json
  53. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ReverseLeft.json
  54. 1
    1
      web/app/components/base/icons/src/vender/line/communication/AiText.json
  55. 1
    1
      web/app/components/base/icons/src/vender/line/communication/ChatBot.json
  56. 1
    1
      web/app/components/base/icons/src/vender/line/communication/ChatBotSlim.json
  57. 1
    1
      web/app/components/base/icons/src/vender/line/communication/CuteRobot.json
  58. 1
    1
      web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.json
  59. 1
    1
      web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.json
  60. 1
    1
      web/app/components/base/icons/src/vender/line/development/ArtificialBrain.json
  61. 1
    1
      web/app/components/base/icons/src/vender/line/development/BarChartSquare02.json
  62. 1
    1
      web/app/components/base/icons/src/vender/line/development/BracketsX.json
  63. 1
    1
      web/app/components/base/icons/src/vender/line/development/CodeBrowser.json
  64. 1
    1
      web/app/components/base/icons/src/vender/line/development/Container.json
  65. 1
    1
      web/app/components/base/icons/src/vender/line/development/Database01.json
  66. 1
    1
      web/app/components/base/icons/src/vender/line/development/Database03.json
  67. 1
    1
      web/app/components/base/icons/src/vender/line/development/FileHeart02.json
  68. 1
    1
      web/app/components/base/icons/src/vender/line/development/GitBranch01.json
  69. 1
    1
      web/app/components/base/icons/src/vender/line/development/PromptEngineering.json
  70. 1
    1
      web/app/components/base/icons/src/vender/line/development/PuzzlePiece01.json
  71. 1
    1
      web/app/components/base/icons/src/vender/line/development/TerminalSquare.json
  72. 1
    1
      web/app/components/base/icons/src/vender/line/development/Variable.json
  73. 1
    1
      web/app/components/base/icons/src/vender/line/development/Webhooks.json
  74. 1
    1
      web/app/components/base/icons/src/vender/line/editor/AlignLeft.json
  75. 1
    1
      web/app/components/base/icons/src/vender/line/editor/BezierCurve03.json
  76. 1
    1
      web/app/components/base/icons/src/vender/line/editor/Collapse.json
  77. 1
    1
      web/app/components/base/icons/src/vender/line/editor/Colors.json
  78. 1
    1
      web/app/components/base/icons/src/vender/line/editor/ImageIndentLeft.json
  79. 1
    1
      web/app/components/base/icons/src/vender/line/editor/LeftIndent02.json
  80. 1
    1
      web/app/components/base/icons/src/vender/line/editor/LetterSpacing01.json
  81. 1
    1
      web/app/components/base/icons/src/vender/line/editor/TypeSquare.json
  82. 1
    1
      web/app/components/base/icons/src/vender/line/education/BookOpen01.json
  83. 1
    1
      web/app/components/base/icons/src/vender/line/files/File02.json
  84. 1
    1
      web/app/components/base/icons/src/vender/line/files/FileArrow01.json
  85. 1
    1
      web/app/components/base/icons/src/vender/line/files/FileCheck02.json
  86. 1
    1
      web/app/components/base/icons/src/vender/line/files/FileDownload02.json
  87. 1
    1
      web/app/components/base/icons/src/vender/line/files/FilePlus01.json
  88. 1
    1
      web/app/components/base/icons/src/vender/line/files/FilePlus02.json
  89. 1
    1
      web/app/components/base/icons/src/vender/line/files/FileText.json
  90. 1
    1
      web/app/components/base/icons/src/vender/line/files/FileUpload.json
  91. 1
    1
      web/app/components/base/icons/src/vender/line/files/Folder.json
  92. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json
  93. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/CoinsStacked01.json
  94. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.json
  95. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.json
  96. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag01.json
  97. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag03.json
  98. 1
    1
      web/app/components/base/icons/src/vender/line/general/AtSign.json
  99. 1
    1
      web/app/components/base/icons/src/vender/line/general/Bookmark.json
  100. 0
    0
      web/app/components/base/icons/src/vender/line/general/Check.json

+ 10
- 0
api/.env.example View File

# Celery beat configuration # Celery beat configuration
CELERY_BEAT_SCHEDULER_TIME=1 CELERY_BEAT_SCHEDULER_TIME=1


# Celery schedule tasks configuration
ENABLE_CLEAN_EMBEDDING_CACHE_TASK=false
ENABLE_CLEAN_UNUSED_DATASETS_TASK=false
ENABLE_CREATE_TIDB_SERVERLESS_TASK=false
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK=false
ENABLE_CLEAN_MESSAGES=false
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK=false
ENABLE_DATASETS_QUEUE_MONITOR=false
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK=true

# Position configuration # Position configuration
POSITION_TOOL_PINS= POSITION_TOOL_PINS=
POSITION_TOOL_INCLUDES= POSITION_TOOL_INCLUDES=

+ 6
- 1
api/README.md View File

10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service. 10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service.


```bash ```bash
uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion
uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion,plugin
```

Addition, if you want to debug the celery scheduled tasks, you can use the following command in another terminal:
```bash
uv run celery -A app.celery beat
``` ```


## Testing ## Testing

+ 36
- 0
api/configs/feature/__init__.py View File

) )




class CeleryScheduleTasksConfig(BaseSettings):
ENABLE_CLEAN_EMBEDDING_CACHE_TASK: bool = Field(
description="Enable clean embedding cache task",
default=False,
)
ENABLE_CLEAN_UNUSED_DATASETS_TASK: bool = Field(
description="Enable clean unused datasets task",
default=False,
)
ENABLE_CREATE_TIDB_SERVERLESS_TASK: bool = Field(
description="Enable create tidb service job task",
default=False,
)
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK: bool = Field(
description="Enable update tidb service job status task",
default=False,
)
ENABLE_CLEAN_MESSAGES: bool = Field(
description="Enable clean messages task",
default=False,
)
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK: bool = Field(
description="Enable mail clean document notify task",
default=False,
)
ENABLE_DATASETS_QUEUE_MONITOR: bool = Field(
description="Enable queue monitor task",
default=False,
)
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK: bool = Field(
description="Enable check upgradable plugin task",
default=True,
)


class PositionConfig(BaseSettings): class PositionConfig(BaseSettings):
POSITION_PROVIDER_PINS: str = Field( POSITION_PROVIDER_PINS: str = Field(
description="Comma-separated list of pinned model providers", description="Comma-separated list of pinned model providers",
# hosted services config # hosted services config
HostedServiceConfig, HostedServiceConfig,
CeleryBeatConfig, CeleryBeatConfig,
CeleryScheduleTasksConfig,
): ):
pass pass

+ 114
- 1
api/controllers/console/workspace/plugin.py View File

from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.plugin.impl.exc import PluginDaemonClientSideError from core.plugin.impl.exc import PluginDaemonClientSideError
from libs.login import login_required from libs.login import login_required
from models.account import TenantPluginPermission
from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
from services.plugin.plugin_parameter_service import PluginParameterService from services.plugin.plugin_parameter_service import PluginParameterService
from services.plugin.plugin_permission_service import PluginPermissionService from services.plugin.plugin_permission_service import PluginPermissionService
from services.plugin.plugin_service import PluginService from services.plugin.plugin_service import PluginService
return jsonable_encoder({"options": options}) return jsonable_encoder({"options": options})




class PluginChangePreferencesApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
user = current_user
if not user.is_admin_or_owner:
raise Forbidden()

req = reqparse.RequestParser()
req.add_argument("permission", type=dict, required=True, location="json")
req.add_argument("auto_upgrade", type=dict, required=True, location="json")
args = req.parse_args()

tenant_id = user.current_tenant_id

permission = args["permission"]

install_permission = TenantPluginPermission.InstallPermission(permission.get("install_permission", "everyone"))
debug_permission = TenantPluginPermission.DebugPermission(permission.get("debug_permission", "everyone"))

auto_upgrade = args["auto_upgrade"]

strategy_setting = TenantPluginAutoUpgradeStrategy.StrategySetting(
auto_upgrade.get("strategy_setting", "fix_only")
)
upgrade_time_of_day = auto_upgrade.get("upgrade_time_of_day", 0)
upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode(auto_upgrade.get("upgrade_mode", "exclude"))
exclude_plugins = auto_upgrade.get("exclude_plugins", [])
include_plugins = auto_upgrade.get("include_plugins", [])

# set permission
set_permission_result = PluginPermissionService.change_permission(
tenant_id,
install_permission,
debug_permission,
)
if not set_permission_result:
return jsonable_encoder({"success": False, "message": "Failed to set permission"})

# set auto upgrade strategy
set_auto_upgrade_strategy_result = PluginAutoUpgradeService.change_strategy(
tenant_id,
strategy_setting,
upgrade_time_of_day,
upgrade_mode,
exclude_plugins,
include_plugins,
)
if not set_auto_upgrade_strategy_result:
return jsonable_encoder({"success": False, "message": "Failed to set auto upgrade strategy"})

return jsonable_encoder({"success": True})


class PluginFetchPreferencesApi(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self):
tenant_id = current_user.current_tenant_id

permission = PluginPermissionService.get_permission(tenant_id)
permission_dict = {
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
}

if permission:
permission_dict["install_permission"] = permission.install_permission
permission_dict["debug_permission"] = permission.debug_permission

auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id)
auto_upgrade_dict = {
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
"upgrade_time_of_day": 0,
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
"exclude_plugins": [],
"include_plugins": [],
}

if auto_upgrade:
auto_upgrade_dict = {
"strategy_setting": auto_upgrade.strategy_setting,
"upgrade_time_of_day": auto_upgrade.upgrade_time_of_day,
"upgrade_mode": auto_upgrade.upgrade_mode,
"exclude_plugins": auto_upgrade.exclude_plugins,
"include_plugins": auto_upgrade.include_plugins,
}

return jsonable_encoder({"permission": permission_dict, "auto_upgrade": auto_upgrade_dict})


class PluginAutoUpgradeExcludePluginApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
# exclude one single plugin
tenant_id = current_user.current_tenant_id

req = reqparse.RequestParser()
req.add_argument("plugin_id", type=str, required=True, location="json")
args = req.parse_args()

return jsonable_encoder({"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args["plugin_id"])})


api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key") api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key")
api.add_resource(PluginListApi, "/workspaces/current/plugin/list") api.add_resource(PluginListApi, "/workspaces/current/plugin/list")
api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions") api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions")
api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch")


api.add_resource(PluginFetchDynamicSelectOptionsApi, "/workspaces/current/plugin/parameters/dynamic-options") api.add_resource(PluginFetchDynamicSelectOptionsApi, "/workspaces/current/plugin/parameters/dynamic-options")

api.add_resource(PluginFetchPreferencesApi, "/workspaces/current/plugin/preferences/fetch")
api.add_resource(PluginChangePreferencesApi, "/workspaces/current/plugin/preferences/change")
api.add_resource(PluginAutoUpgradeExcludePluginApi, "/workspaces/current/plugin/preferences/autoupgrade/exclude")

+ 20
- 0
api/core/helper/marketplace.py View File

url = str(marketplace_api_url / "api/v1/plugins/batch") url = str(marketplace_api_url / "api/v1/plugins/batch")
response = requests.post(url, json={"plugin_ids": plugin_ids}) response = requests.post(url, json={"plugin_ids": plugin_ids})
response.raise_for_status() response.raise_for_status()

return [MarketplacePluginDeclaration(**plugin) for plugin in response.json()["data"]["plugins"]] return [MarketplacePluginDeclaration(**plugin) for plugin in response.json()["data"]["plugins"]]




def batch_fetch_plugin_manifests_ignore_deserialization_error(
plugin_ids: list[str],
) -> Sequence[MarketplacePluginDeclaration]:
if len(plugin_ids) == 0:
return []

url = str(marketplace_api_url / "api/v1/plugins/batch")
response = requests.post(url, json={"plugin_ids": plugin_ids})
response.raise_for_status()
result: list[MarketplacePluginDeclaration] = []
for plugin in response.json()["data"]["plugins"]:
try:
result.append(MarketplacePluginDeclaration(**plugin))
except Exception as e:
pass

return result


def record_install_plugin_event(plugin_unique_identifier: str): def record_install_plugin_event(plugin_unique_identifier: str):
url = str(marketplace_api_url / "api/v1/stats/plugins/install_count") url = str(marketplace_api_url / "api/v1/stats/plugins/install_count")
response = requests.post(url, json={"unique_identifier": plugin_unique_identifier}) response = requests.post(url, json={"unique_identifier": plugin_unique_identifier})

+ 1
- 1
api/docker/entrypoint.sh View File



exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION \ exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION \
--max-tasks-per-child ${MAX_TASK_PRE_CHILD:-50} --loglevel ${LOG_LEVEL:-INFO} \ --max-tasks-per-child ${MAX_TASK_PRE_CHILD:-50} --loglevel ${LOG_LEVEL:-INFO} \
-Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion}
-Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion,plugin}


elif [[ "${MODE}" == "beat" ]]; then elif [[ "${MODE}" == "beat" ]]; then
exec celery -A app.celery beat --loglevel ${LOG_LEVEL:-INFO} exec celery -A app.celery beat --loglevel ${LOG_LEVEL:-INFO}

+ 39
- 26
api/extensions/ext_celery.py View File

celery_app.set_default() celery_app.set_default()
app.extensions["celery"] = celery_app app.extensions["celery"] = celery_app


imports = [
"schedule.clean_embedding_cache_task",
"schedule.clean_unused_datasets_task",
"schedule.create_tidb_serverless_task",
"schedule.update_tidb_serverless_status_task",
"schedule.clean_messages",
"schedule.mail_clean_document_notify_task",
"schedule.queue_monitor_task",
]
imports = []
day = dify_config.CELERY_BEAT_SCHEDULER_TIME day = dify_config.CELERY_BEAT_SCHEDULER_TIME
beat_schedule = {
"clean_embedding_cache_task": {

# if you add a new task, please add the switch to CeleryScheduleTasksConfig
beat_schedule = {}
if dify_config.ENABLE_CLEAN_EMBEDDING_CACHE_TASK:
imports.append("schedule.clean_embedding_cache_task")
beat_schedule["clean_embedding_cache_task"] = {
"task": "schedule.clean_embedding_cache_task.clean_embedding_cache_task", "task": "schedule.clean_embedding_cache_task.clean_embedding_cache_task",
"schedule": timedelta(days=day), "schedule": timedelta(days=day),
},
"clean_unused_datasets_task": {
}
if dify_config.ENABLE_CLEAN_UNUSED_DATASETS_TASK:
imports.append("schedule.clean_unused_datasets_task")
beat_schedule["clean_unused_datasets_task"] = {
"task": "schedule.clean_unused_datasets_task.clean_unused_datasets_task", "task": "schedule.clean_unused_datasets_task.clean_unused_datasets_task",
"schedule": timedelta(days=day), "schedule": timedelta(days=day),
},
"create_tidb_serverless_task": {
}
if dify_config.ENABLE_CREATE_TIDB_SERVERLESS_TASK:
imports.append("schedule.create_tidb_serverless_task")
beat_schedule["create_tidb_serverless_task"] = {
"task": "schedule.create_tidb_serverless_task.create_tidb_serverless_task", "task": "schedule.create_tidb_serverless_task.create_tidb_serverless_task",
"schedule": crontab(minute="0", hour="*"), "schedule": crontab(minute="0", hour="*"),
},
"update_tidb_serverless_status_task": {
}
if dify_config.ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK:
imports.append("schedule.update_tidb_serverless_status_task")
beat_schedule["update_tidb_serverless_status_task"] = {
"task": "schedule.update_tidb_serverless_status_task.update_tidb_serverless_status_task", "task": "schedule.update_tidb_serverless_status_task.update_tidb_serverless_status_task",
"schedule": timedelta(minutes=10), "schedule": timedelta(minutes=10),
},
"clean_messages": {
}
if dify_config.ENABLE_CLEAN_MESSAGES:
imports.append("schedule.clean_messages")
beat_schedule["clean_messages"] = {
"task": "schedule.clean_messages.clean_messages", "task": "schedule.clean_messages.clean_messages",
"schedule": timedelta(days=day), "schedule": timedelta(days=day),
},
# every Monday
"mail_clean_document_notify_task": {
}
if dify_config.ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK:
imports.append("schedule.mail_clean_document_notify_task")
beat_schedule["mail_clean_document_notify_task"] = {
"task": "schedule.mail_clean_document_notify_task.mail_clean_document_notify_task", "task": "schedule.mail_clean_document_notify_task.mail_clean_document_notify_task",
"schedule": crontab(minute="0", hour="10", day_of_week="1"), "schedule": crontab(minute="0", hour="10", day_of_week="1"),
},
"datasets-queue-monitor": {
}
if dify_config.ENABLE_DATASETS_QUEUE_MONITOR:
imports.append("schedule.queue_monitor_task")
beat_schedule["datasets-queue-monitor"] = {
"task": "schedule.queue_monitor_task.queue_monitor_task", "task": "schedule.queue_monitor_task.queue_monitor_task",
"schedule": timedelta( "schedule": timedelta(
minutes=dify_config.QUEUE_MONITOR_INTERVAL if dify_config.QUEUE_MONITOR_INTERVAL else 30 minutes=dify_config.QUEUE_MONITOR_INTERVAL if dify_config.QUEUE_MONITOR_INTERVAL else 30
), ),
},
}
}
if dify_config.ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK:
imports.append("schedule.check_upgradable_plugin_task")
beat_schedule["check_upgradable_plugin_task"] = {
"task": "schedule.check_upgradable_plugin_task.check_upgradable_plugin_task",
"schedule": crontab(minute="*/15"),
}

celery_app.conf.update(beat_schedule=beat_schedule, imports=imports) celery_app.conf.update(beat_schedule=beat_schedule, imports=imports)


return celery_app return celery_app

+ 42
- 0
api/migrations/versions/2025_07_23_1508-8bcc02c9bd07_add_tenant_plugin_autoupgrade_table.py View File

"""add_tenant_plugin_autoupgrade_table

Revision ID: 8bcc02c9bd07
Revises: 375fe79ead14
Create Date: 2025-07-23 15:08:50.161441

"""
from alembic import op
import models as models
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '8bcc02c9bd07'
down_revision = '375fe79ead14'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('tenant_plugin_auto_upgrade_strategies',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
sa.Column('strategy_setting', sa.String(length=16), server_default='fix_only', nullable=False),
sa.Column('upgrade_time_of_day', sa.Integer(), nullable=False),
sa.Column('upgrade_mode', sa.String(length=16), server_default='exclude', nullable=False),
sa.Column('exclude_plugins', sa.ARRAY(sa.String(length=255)), nullable=False),
sa.Column('include_plugins', sa.ARRAY(sa.String(length=255)), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id', name='tenant_plugin_auto_upgrade_strategy_pkey'),
sa.UniqueConstraint('tenant_id', name='unique_tenant_plugin_auto_upgrade_strategy')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('tenant_plugin_auto_upgrade_strategies')
# ### end Alembic commands ###

+ 37
- 3
api/models/account.py View File

) )


id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
tenant_id: Mapped[str] = mapped_column(StringUUID)
install_permission: Mapped[InstallPermission] = mapped_column(db.String(16), server_default="everyone")
debug_permission: Mapped[DebugPermission] = mapped_column(db.String(16), server_default="noone")
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
install_permission: Mapped[InstallPermission] = mapped_column(
db.String(16), nullable=False, server_default="everyone"
)
debug_permission: Mapped[DebugPermission] = mapped_column(db.String(16), nullable=False, server_default="noone")


class TenantPluginAutoUpgradeStrategy(Base):
class StrategySetting(enum.StrEnum):
DISABLED = "disabled"
FIX_ONLY = "fix_only"
LATEST = "latest"

class UpgradeMode(enum.StrEnum):
ALL = "all"
PARTIAL = "partial"
EXCLUDE = "exclude"

__tablename__ = "tenant_plugin_auto_upgrade_strategies"
__table_args__ = (
db.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"),
db.UniqueConstraint("tenant_id", name="unique_tenant_plugin_auto_upgrade_strategy"),
)

id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
strategy_setting: Mapped[StrategySetting] = mapped_column(db.String(16), nullable=False, server_default="fix_only")
upgrade_time_of_day: Mapped[int] = mapped_column(db.Integer, nullable=False, default=0) # seconds of the day
upgrade_mode: Mapped[UpgradeMode] = mapped_column(db.String(16), nullable=False, server_default="exclude")
exclude_plugins: Mapped[list[str]] = mapped_column(
db.ARRAY(db.String(255)), nullable=False
) # plugin_id (author/name)
include_plugins: Mapped[list[str]] = mapped_column(
db.ARRAY(db.String(255)), nullable=False
) # plugin_id (author/name)
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())

+ 49
- 0
api/schedule/check_upgradable_plugin_task.py View File

import time

import click

import app
from extensions.ext_database import db
from models.account import TenantPluginAutoUpgradeStrategy
from tasks.process_tenant_plugin_autoupgrade_check_task import process_tenant_plugin_autoupgrade_check_task

AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL = 15 * 60 # 15 minutes


@app.celery.task(queue="plugin")
def check_upgradable_plugin_task():
click.echo(click.style("Start check upgradable plugin.", fg="green"))
start_at = time.perf_counter()

now_seconds_of_day = time.time() % 86400 - 30 # we assume the tz is UTC
click.echo(click.style("Now seconds of day: {}".format(now_seconds_of_day), fg="green"))

strategies = (
db.session.query(TenantPluginAutoUpgradeStrategy)
.filter(
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day >= now_seconds_of_day,
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day
< now_seconds_of_day + AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL,
TenantPluginAutoUpgradeStrategy.strategy_setting
!= TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
)
.all()
)

for strategy in strategies:
process_tenant_plugin_autoupgrade_check_task.delay(
strategy.tenant_id,
strategy.strategy_setting,
strategy.upgrade_time_of_day,
strategy.upgrade_mode,
strategy.exclude_plugins,
strategy.include_plugins,
)

end_at = time.perf_counter()
click.echo(
click.style(
"Checked upgradable plugin success latency: {}".format(end_at - start_at),
fg="green",
)
)

+ 12
- 0
api/services/account_service.py View File

Tenant, Tenant,
TenantAccountJoin, TenantAccountJoin,
TenantAccountRole, TenantAccountRole,
TenantPluginAutoUpgradeStrategy,
TenantStatus, TenantStatus,
) )
from models.model import DifySetup from models.model import DifySetup
db.session.add(tenant) db.session.add(tenant)
db.session.commit() db.session.commit()


plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy(
tenant_id=tenant.id,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
exclude_plugins=[],
include_plugins=[],
)
db.session.add(plugin_upgrade_strategy)
db.session.commit()

tenant.encrypt_public_key = generate_key_pair(tenant.id) tenant.encrypt_public_key = generate_key_pair(tenant.id)
db.session.commit() db.session.commit()
return tenant return tenant

+ 87
- 0
api/services/plugin/plugin_auto_upgrade_service.py View File

from sqlalchemy.orm import Session

from extensions.ext_database import db
from models.account import TenantPluginAutoUpgradeStrategy


class PluginAutoUpgradeService:
@staticmethod
def get_strategy(tenant_id: str) -> TenantPluginAutoUpgradeStrategy | None:
with Session(db.engine) as session:
return (
session.query(TenantPluginAutoUpgradeStrategy)
.filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
.first()
)

@staticmethod
def change_strategy(
tenant_id: str,
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
upgrade_time_of_day: int,
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
exclude_plugins: list[str],
include_plugins: list[str],
) -> bool:
with Session(db.engine) as session:
exist_strategy = (
session.query(TenantPluginAutoUpgradeStrategy)
.filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
.first()
)
if not exist_strategy:
strategy = TenantPluginAutoUpgradeStrategy(
tenant_id=tenant_id,
strategy_setting=strategy_setting,
upgrade_time_of_day=upgrade_time_of_day,
upgrade_mode=upgrade_mode,
exclude_plugins=exclude_plugins,
include_plugins=include_plugins,
)
session.add(strategy)
else:
exist_strategy.strategy_setting = strategy_setting
exist_strategy.upgrade_time_of_day = upgrade_time_of_day
exist_strategy.upgrade_mode = upgrade_mode
exist_strategy.exclude_plugins = exclude_plugins
exist_strategy.include_plugins = include_plugins

session.commit()
return True

@staticmethod
def exclude_plugin(tenant_id: str, plugin_id: str) -> bool:
with Session(db.engine) as session:
exist_strategy = (
session.query(TenantPluginAutoUpgradeStrategy)
.filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
.first()
)
if not exist_strategy:
# create for this tenant
PluginAutoUpgradeService.change_strategy(
tenant_id,
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
0,
TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
[plugin_id],
[],
)
return True
else:
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
if plugin_id not in exist_strategy.exclude_plugins:
new_exclude_plugins = exist_strategy.exclude_plugins.copy()
new_exclude_plugins.append(plugin_id)
exist_strategy.exclude_plugins = new_exclude_plugins
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL:
if plugin_id in exist_strategy.include_plugins:
new_include_plugins = exist_strategy.include_plugins.copy()
new_include_plugins.remove(plugin_id)
exist_strategy.include_plugins = new_include_plugins
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
exist_strategy.exclude_plugins = [plugin_id]

session.commit()
return True

+ 166
- 0
api/tasks/process_tenant_plugin_autoupgrade_check_task.py View File

import traceback
import typing

import click
from celery import shared_task # type: ignore

from core.helper import marketplace
from core.helper.marketplace import MarketplacePluginDeclaration
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.impl.plugin import PluginInstaller
from models.account import TenantPluginAutoUpgradeStrategy

RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3


cached_plugin_manifests: dict[str, typing.Union[MarketplacePluginDeclaration, None]] = {}


def marketplace_batch_fetch_plugin_manifests(
plugin_ids_plain_list: list[str],
) -> list[MarketplacePluginDeclaration]:
global cached_plugin_manifests
# return marketplace.batch_fetch_plugin_manifests(plugin_ids_plain_list)
not_included_plugin_ids = [
plugin_id for plugin_id in plugin_ids_plain_list if plugin_id not in cached_plugin_manifests
]
if not_included_plugin_ids:
manifests = marketplace.batch_fetch_plugin_manifests_ignore_deserialization_error(not_included_plugin_ids)
for manifest in manifests:
cached_plugin_manifests[manifest.plugin_id] = manifest

if (
len(manifests) == 0
): # this indicates that the plugin not found in marketplace, should set None in cache to prevent future check
for plugin_id in not_included_plugin_ids:
cached_plugin_manifests[plugin_id] = None

result: list[MarketplacePluginDeclaration] = []
for plugin_id in plugin_ids_plain_list:
final_manifest = cached_plugin_manifests.get(plugin_id)
if final_manifest is not None:
result.append(final_manifest)

return result


@shared_task(queue="plugin")
def process_tenant_plugin_autoupgrade_check_task(
tenant_id: str,
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
upgrade_time_of_day: int,
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
exclude_plugins: list[str],
include_plugins: list[str],
):
try:
manager = PluginInstaller()

click.echo(
click.style(
"Checking upgradable plugin for tenant: {}".format(tenant_id),
fg="green",
)
)

if strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED:
return

# get plugin_ids to check
plugin_ids: list[tuple[str, str, str]] = [] # plugin_id, version, unique_identifier
click.echo(click.style("Upgrade mode: {}".format(upgrade_mode), fg="green"))

if upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL and include_plugins:
all_plugins = manager.list_plugins(tenant_id)

for plugin in all_plugins:
if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id in include_plugins:
plugin_ids.append(
(
plugin.plugin_id,
plugin.version,
plugin.plugin_unique_identifier,
)
)

elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
# get all plugins and remove excluded plugins
all_plugins = manager.list_plugins(tenant_id)
plugin_ids = [
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
for plugin in all_plugins
if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id not in exclude_plugins
]
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
all_plugins = manager.list_plugins(tenant_id)
plugin_ids = [
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
for plugin in all_plugins
if plugin.source == PluginInstallationSource.Marketplace
]

if not plugin_ids:
return

plugin_ids_plain_list = [plugin_id for plugin_id, _, _ in plugin_ids]

manifests = marketplace_batch_fetch_plugin_manifests(plugin_ids_plain_list)

if not manifests:
return

for manifest in manifests:
for plugin_id, version, original_unique_identifier in plugin_ids:
if manifest.plugin_id != plugin_id:
continue

try:
current_version = version
latest_version = manifest.latest_version

def fix_only_checker(latest_version, current_version):
latest_version_tuple = tuple(int(val) for val in latest_version.split("."))
current_version_tuple = tuple(int(val) for val in current_version.split("."))

if (
latest_version_tuple[0] == current_version_tuple[0]
and latest_version_tuple[1] == current_version_tuple[1]
):
return latest_version_tuple[2] != current_version_tuple[2]
return False

version_checker = {
TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST: lambda latest_version,
current_version: latest_version != current_version,
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY: fix_only_checker,
}

if version_checker[strategy_setting](latest_version, current_version):
# execute upgrade
new_unique_identifier = manifest.latest_package_identifier

marketplace.record_install_plugin_event(new_unique_identifier)
click.echo(
click.style(
"Upgrade plugin: {} -> {}".format(original_unique_identifier, new_unique_identifier),
fg="green",
)
)
task_start_resp = manager.upgrade_plugin(
tenant_id,
original_unique_identifier,
new_unique_identifier,
PluginInstallationSource.Marketplace,
{
"plugin_unique_identifier": new_unique_identifier,
},
)
except Exception as e:
click.echo(click.style("Error when upgrading plugin: {}".format(e), fg="red"))
traceback.print_exc()
break

except Exception as e:
click.echo(click.style("Error when checking upgradable plugin: {}".format(e), fg="red"))
traceback.print_exc()
return

+ 10
- 0
docker/.env.example View File

QUEUE_MONITOR_ALERT_EMAILS= QUEUE_MONITOR_ALERT_EMAILS=
# Monitor interval in minutes, default is 30 minutes # Monitor interval in minutes, default is 30 minutes
QUEUE_MONITOR_INTERVAL=30 QUEUE_MONITOR_INTERVAL=30

# Celery schedule tasks configuration
ENABLE_CLEAN_EMBEDDING_CACHE_TASK=false
ENABLE_CLEAN_UNUSED_DATASETS_TASK=false
ENABLE_CREATE_TIDB_SERVERLESS_TASK=false
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK=false
ENABLE_CLEAN_MESSAGES=false
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK=false
ENABLE_DATASETS_QUEUE_MONITOR=false
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK=true

+ 19
- 0
docker/docker-compose-template.yaml View File

- ssrf_proxy_network - ssrf_proxy_network
- default - default


# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
image: langgenius/dify-api:1.5.0
restart: always
environment:
# Use the shared environment variables.
<<: *shared-api-worker-env
# Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks.
MODE: beat
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- ssrf_proxy_network
- default

# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:1.6.0 image: langgenius/dify-web:1.6.0

+ 27
- 0
docker/docker-compose.yaml View File

QUEUE_MONITOR_THRESHOLD: ${QUEUE_MONITOR_THRESHOLD:-200} QUEUE_MONITOR_THRESHOLD: ${QUEUE_MONITOR_THRESHOLD:-200}
QUEUE_MONITOR_ALERT_EMAILS: ${QUEUE_MONITOR_ALERT_EMAILS:-} QUEUE_MONITOR_ALERT_EMAILS: ${QUEUE_MONITOR_ALERT_EMAILS:-}
QUEUE_MONITOR_INTERVAL: ${QUEUE_MONITOR_INTERVAL:-30} QUEUE_MONITOR_INTERVAL: ${QUEUE_MONITOR_INTERVAL:-30}
ENABLE_CLEAN_EMBEDDING_CACHE_TASK: ${ENABLE_CLEAN_EMBEDDING_CACHE_TASK:-false}
ENABLE_CLEAN_UNUSED_DATASETS_TASK: ${ENABLE_CLEAN_UNUSED_DATASETS_TASK:-false}
ENABLE_CREATE_TIDB_SERVERLESS_TASK: ${ENABLE_CREATE_TIDB_SERVERLESS_TASK:-false}
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK: ${ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK:-false}
ENABLE_CLEAN_MESSAGES: ${ENABLE_CLEAN_MESSAGES:-false}
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK: ${ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK:-false}
ENABLE_DATASETS_QUEUE_MONITOR: ${ENABLE_DATASETS_QUEUE_MONITOR:-false}
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK: ${ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK:-true}


services: services:
# API service # API service
- ssrf_proxy_network - ssrf_proxy_network
- default - default


# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
image: langgenius/dify-api:1.5.0
restart: always
environment:
# Use the shared environment variables.
<<: *shared-api-worker-env
# Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks.
MODE: beat
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- ssrf_proxy_network
- default

# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:1.6.0 image: langgenius/dify-web:1.6.0

+ 4
- 1
web/app/components/base/date-and-time-picker/common/option-list-item.tsx View File

type OptionListItemProps = { type OptionListItemProps = {
isSelected: boolean isSelected: boolean
onClick: () => void onClick: () => void
noAutoScroll?: boolean
} & React.LiHTMLAttributes<HTMLLIElement> } & React.LiHTMLAttributes<HTMLLIElement>


const OptionListItem: FC<OptionListItemProps> = ({ const OptionListItem: FC<OptionListItemProps> = ({
isSelected, isSelected,
onClick, onClick,
noAutoScroll,
children, children,
}) => { }) => {
const listItemRef = useRef<HTMLLIElement>(null) const listItemRef = useRef<HTMLLIElement>(null)


useEffect(() => { useEffect(() => {
if (isSelected)
if (isSelected && !noAutoScroll)
listItemRef.current?.scrollIntoView({ behavior: 'instant' }) listItemRef.current?.scrollIntoView({ behavior: 'instant' })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])


return ( return (

+ 7
- 2
web/app/components/base/date-and-time-picker/time-picker/header.tsx View File

import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'


const Header = () => {
type Props = {
title?: string
}
const Header = ({
title,
}: Props) => {
const { t } = useTranslation() const { t } = useTranslation()


return ( return (
<div className='flex flex-col border-b-[0.5px] border-divider-regular'> <div className='flex flex-col border-b-[0.5px] border-divider-regular'>
<div className='system-md-semibold flex items-center px-2 py-1.5 text-text-primary'> <div className='system-md-semibold flex items-center px-2 py-1.5 text-text-primary'>
{t('time.title.pickTime')}
{title || t('time.title.pickTime')}
</div> </div>
</div> </div>
) )

+ 21
- 10
web/app/components/base/date-and-time-picker/time-picker/index.tsx View File

onChange, onChange,
onClear, onClear,
renderTrigger, renderTrigger,
title,
minuteFilter,
popupClassName,
}: TimePickerProps) => { }: TimePickerProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const displayValue = value?.format(timeFormat) || '' const displayValue = value?.format(timeFormat) || ''
const placeholderDate = isOpen && selectedTime ? selectedTime.format(timeFormat) : (placeholder || t('time.defaultPlaceholder')) const placeholderDate = isOpen && selectedTime ? selectedTime.format(timeFormat) : (placeholder || t('time.defaultPlaceholder'))


const inputElem = (
<input
className='system-xs-regular flex-1 cursor-pointer appearance-none truncate bg-transparent p-1
text-components-input-text-filled outline-none placeholder:text-components-input-text-placeholder'
readOnly
value={isOpen ? '' : displayValue}
placeholder={placeholderDate}
/>
)
return ( return (
<PortalToFollowElem <PortalToFollowElem
open={isOpen} open={isOpen}
placement='bottom-end' placement='bottom-end'
> >
<PortalToFollowElemTrigger> <PortalToFollowElemTrigger>
{renderTrigger ? (renderTrigger()) : (
{renderTrigger ? (renderTrigger({
inputElem,
onClick: handleClickTrigger,
isOpen,
})) : (
<div <div
className='group flex w-[252px] cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt' className='group flex w-[252px] cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt'
onClick={handleClickTrigger} onClick={handleClickTrigger}
> >
<input
className='system-xs-regular flex-1 cursor-pointer appearance-none truncate bg-transparent p-1
text-components-input-text-filled outline-none placeholder:text-components-input-text-placeholder'
readOnly
value={isOpen ? '' : displayValue}
placeholder={placeholderDate}
/>
{inputElem}
<RiTimeLine className={cn( <RiTimeLine className={cn(
'h-4 w-4 shrink-0 text-text-quaternary', 'h-4 w-4 shrink-0 text-text-quaternary',
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary', isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
</div> </div>
)} )}
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<PortalToFollowElemContent className={cn('z-50', popupClassName)}>
<div className='mt-1 w-[252px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg shadow-shadow-shadow-5'> <div className='mt-1 w-[252px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg shadow-shadow-shadow-5'>
{/* Header */} {/* Header */}
<Header />
<Header title={title} />


{/* Time Options */} {/* Time Options */}
<Options <Options
selectedTime={selectedTime} selectedTime={selectedTime}
minuteFilter={minuteFilter}
handleSelectHour={handleSelectHour} handleSelectHour={handleSelectHour}
handleSelectMinute={handleSelectMinute} handleSelectMinute={handleSelectMinute}
handleSelectPeriod={handleSelectPeriod} handleSelectPeriod={handleSelectPeriod}

+ 3
- 1
web/app/components/base/date-and-time-picker/time-picker/options.tsx View File



const Options: FC<TimeOptionsProps> = ({ const Options: FC<TimeOptionsProps> = ({
selectedTime, selectedTime,
minuteFilter,
handleSelectHour, handleSelectHour,
handleSelectMinute, handleSelectMinute,
handleSelectPeriod, handleSelectPeriod,
{/* Minute */} {/* Minute */}
<ul className='no-scrollbar flex h-[208px] flex-col gap-y-0.5 overflow-y-auto pb-[184px]'> <ul className='no-scrollbar flex h-[208px] flex-col gap-y-0.5 overflow-y-auto pb-[184px]'>
{ {
minuteOptions.map((minute) => {
(minuteFilter ? minuteFilter(minuteOptions) : minuteOptions).map((minute) => {
const isSelected = selectedTime?.format('mm') === minute const isSelected = selectedTime?.format('mm') === minute
return ( return (
<OptionListItem <OptionListItem
key={period} key={period}
isSelected={isSelected} isSelected={isSelected}
onClick={handleSelectPeriod.bind(null, period)} onClick={handleSelectPeriod.bind(null, period)}
noAutoScroll // if choose PM which would hide(scrolled) AM that may make user confused that there's no am.
> >
{period} {period}
</OptionListItem> </OptionListItem>

+ 11
- 1
web/app/components/base/date-and-time-picker/types.ts View File

onClear: () => void onClear: () => void
triggerWrapClassName?: string triggerWrapClassName?: string
renderTrigger?: (props: TriggerProps) => React.ReactNode renderTrigger?: (props: TriggerProps) => React.ReactNode
minuteFilter?: (minutes: string[]) => string[]
popupZIndexClassname?: string popupZIndexClassname?: string
} }


handleConfirmDate: () => void handleConfirmDate: () => void
} }


export type TriggerParams = {
isOpen: boolean
inputElem: React.ReactNode
onClick: (e: React.MouseEvent) => void
}
export type TimePickerProps = { export type TimePickerProps = {
value: Dayjs | undefined value: Dayjs | undefined
timezone?: string timezone?: string
placeholder?: string placeholder?: string
onChange: (date: Dayjs | undefined) => void onChange: (date: Dayjs | undefined) => void
onClear: () => void onClear: () => void
renderTrigger?: () => React.ReactNode
renderTrigger?: (props: TriggerParams) => React.ReactNode
title?: string
minuteFilter?: (minutes: string[]) => string[]
popupClassName?: string
} }


export type TimePickerFooterProps = { export type TimePickerFooterProps = {


export type TimeOptionsProps = { export type TimeOptionsProps = {
selectedTime: Dayjs | undefined selectedTime: Dayjs | undefined
minuteFilter?: (minutes: string[]) => string[]
handleSelectHour: (hour: string) => void handleSelectHour: (hour: string) => void
handleSelectMinute: (minute: string) => void handleSelectMinute: (minute: string) => void
handleSelectPeriod: (period: Period) => void handleSelectPeriod: (period: Period) => void

+ 12
- 0
web/app/components/base/date-and-time-picker/utils/dayjs.ts View File

import type { Day } from '../types' import type { Day } from '../types'
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone' import timezone from 'dayjs/plugin/timezone'
import tz from '@/utils/timezone.json'


dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => { export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => {
return props.date ? dayjs.tz(props.date, props.timezone) : dayjs().tz(props.timezone) return props.date ? dayjs.tz(props.date, props.timezone) : dayjs().tz(props.timezone)
} }

// Asia/Shanghai -> UTC+8
const DEFAULT_OFFSET_STR = 'UTC+0'
export const convertTimezoneToOffsetStr = (timezone?: string) => {
if (!timezone)
return DEFAULT_OFFSET_STR
const tzItem = tz.find(item => item.value === timezone)
if(!tzItem)
return DEFAULT_OFFSET_STR
return `UTC${tzItem.name.charAt(0)}${tzItem.name.charAt(2)}`
}

+ 7
- 0
web/app/components/base/icons/assets/vender/line/general/search-menu.svg View File

<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 16H6.67155" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 9.33334H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 22.6667H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M26 22L29.3333 25.3333" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

+ 4
- 0
web/app/components/base/icons/assets/vender/system/auto-update-line.svg View File

<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3308 16H14.2915L13.6249 13.9476H10.3761L9.70846 16H7.66918L10.7759 7H13.2281L16.3308 16ZM10.8595 12.4622H13.1435L12.0378 9.05639H11.9673L10.8595 12.4622Z" fill="black"/>
</svg>

+ 1
- 1
web/app/components/base/icons/script.mjs View File

export default Icon export default Icon
`.trim()) `.trim())


await writeFile(path.resolve(currentPath, `${fileName}.json`), JSON.stringify(svgData, '', '\t'))
await writeFile(path.resolve(currentPath, `${fileName}.json`), `${JSON.stringify(svgData, '', '\t')}\n`)
await writeFile(path.resolve(currentPath, `${fileName}.tsx`), `${componentRender({ svgName: fileName })}\n`) await writeFile(path.resolve(currentPath, `${fileName}.tsx`), `${componentRender({ svgName: fileName })}\n`)


const indexingRender = template(` const indexingRender = template(`

+ 129
- 116
web/app/components/base/icons/src/public/tracing/AliyunIcon.json
File diff suppressed because it is too large
View File


+ 9
- 5
web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx View File

import * as React from 'react' import * as React from 'react'
import data from './AliyunIcon.json' import data from './AliyunIcon.json'
import IconBase from '@/app/components/base/icons/IconBase' import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'


const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />


Icon.displayName = 'AliyunIcon' Icon.displayName = 'AliyunIcon'



+ 115
- 69
web/app/components/base/icons/src/public/tracing/AliyunIconBig.json
File diff suppressed because it is too large
View File


+ 9
- 5
web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx View File

import * as React from 'react' import * as React from 'react'
import data from './AliyunIconBig.json' import data from './AliyunIconBig.json'
import IconBase from '@/app/components/base/icons/IconBase' import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'


const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />


Icon.displayName = 'AliyunIconBig' Icon.displayName = 'AliyunIconBig'



+ 9
- 5
web/app/components/base/icons/src/public/tracing/WeaveIcon.tsx View File

import * as React from 'react' import * as React from 'react'
import data from './WeaveIcon.json' import data from './WeaveIcon.json'
import IconBase from '@/app/components/base/icons/IconBase' import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'


const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />


Icon.displayName = 'WeaveIcon' Icon.displayName = 'WeaveIcon'



+ 9
- 5
web/app/components/base/icons/src/public/tracing/WeaveIconBig.tsx View File

import * as React from 'react' import * as React from 'react'
import data from './WeaveIconBig.json' import data from './WeaveIconBig.json'
import IconBase from '@/app/components/base/icons/IconBase' import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'


const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />


Icon.displayName = 'WeaveIconBig' Icon.displayName = 'WeaveIconBig'



+ 2
- 2
web/app/components/base/icons/src/public/tracing/index.ts View File

export { default as AliyunIconBig } from './AliyunIconBig'
export { default as AliyunIcon } from './AliyunIcon'
export { default as ArizeIconBig } from './ArizeIconBig' export { default as ArizeIconBig } from './ArizeIconBig'
export { default as ArizeIcon } from './ArizeIcon' export { default as ArizeIcon } from './ArizeIcon'
export { default as LangfuseIconBig } from './LangfuseIconBig' export { default as LangfuseIconBig } from './LangfuseIconBig'
export { default as TracingIcon } from './TracingIcon' export { default as TracingIcon } from './TracingIcon'
export { default as WeaveIconBig } from './WeaveIconBig' export { default as WeaveIconBig } from './WeaveIconBig'
export { default as WeaveIcon } from './WeaveIcon' export { default as WeaveIcon } from './WeaveIcon'
export { default as AliyunIconBig } from './AliyunIconBig'
export { default as AliyunIcon } from './AliyunIcon'

+ 1
- 1
web/app/components/base/icons/src/vender/features/Citations.json View File

] ]
}, },
"name": "Citations" "name": "Citations"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/features/ContentModeration.json View File

] ]
}, },
"name": "ContentModeration" "name": "ContentModeration"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/features/Document.json View File

] ]
}, },
"name": "Document" "name": "Document"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/features/FolderUpload.json View File

] ]
}, },
"name": "FolderUpload" "name": "FolderUpload"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/features/LoveMessage.json View File

] ]
}, },
"name": "LoveMessage" "name": "LoveMessage"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/features/MessageFast.json View File

] ]
}, },
"name": "MessageFast" "name": "MessageFast"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/features/Microphone01.json View File

] ]
}, },
"name": "Microphone01" "name": "Microphone01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/features/TextToAudio.json View File

] ]
}, },
"name": "TextToAudio" "name": "TextToAudio"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/features/VirtualAssistant.json View File

] ]
}, },
"name": "VirtualAssistant" "name": "VirtualAssistant"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/features/Vision.json View File

] ]
}, },
"name": "Vision" "name": "Vision"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/alertsAndFeedback/AlertTriangle.json View File

] ]
}, },
"name": "AlertTriangle" "name": "AlertTriangle"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json View File

] ]
}, },
"name": "ThumbsDown" "name": "ThumbsDown"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json View File

] ]
}, },
"name": "ThumbsUp" "name": "ThumbsUp"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ArrowNarrowLeft.json View File

] ]
}, },
"name": "ArrowNarrowLeft" "name": "ArrowNarrowLeft"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ArrowUpRight.json View File

] ]
}, },
"name": "ArrowUpRight" "name": "ArrowUpRight"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ChevronDownDouble.json View File

] ]
}, },
"name": "ChevronDownDouble" "name": "ChevronDownDouble"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ChevronRight.json View File

] ]
}, },
"name": "ChevronRight" "name": "ChevronRight"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.json View File

] ]
}, },
"name": "ChevronSelectorVertical" "name": "ChevronSelectorVertical"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/RefreshCcw01.json View File

] ]
}, },
"name": "RefreshCcw01" "name": "RefreshCcw01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/RefreshCw05.json View File

] ]
}, },
"name": "RefreshCw05" "name": "RefreshCw05"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ReverseLeft.json View File

] ]
}, },
"name": "ReverseLeft" "name": "ReverseLeft"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/AiText.json View File

] ]
}, },
"name": "AiText" "name": "AiText"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/ChatBot.json View File

] ]
}, },
"name": "ChatBot" "name": "ChatBot"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/ChatBotSlim.json View File

] ]
}, },
"name": "ChatBotSlim" "name": "ChatBotSlim"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/CuteRobot.json View File

] ]
}, },
"name": "CuteRobot" "name": "CuteRobot"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.json View File

] ]
}, },
"name": "MessageCheckRemove" "name": "MessageCheckRemove"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.json View File

] ]
}, },
"name": "MessageFastPlus" "name": "MessageFastPlus"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/ArtificialBrain.json View File

] ]
}, },
"name": "ArtificialBrain" "name": "ArtificialBrain"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/BarChartSquare02.json View File

] ]
}, },
"name": "BarChartSquare02" "name": "BarChartSquare02"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/BracketsX.json View File

] ]
}, },
"name": "BracketsX" "name": "BracketsX"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/CodeBrowser.json View File

] ]
}, },
"name": "CodeBrowser" "name": "CodeBrowser"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/Container.json View File

] ]
}, },
"name": "Container" "name": "Container"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/Database01.json View File

] ]
}, },
"name": "Database01" "name": "Database01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/Database03.json View File

] ]
}, },
"name": "Database03" "name": "Database03"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/FileHeart02.json View File

] ]
}, },
"name": "FileHeart02" "name": "FileHeart02"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/GitBranch01.json View File

] ]
}, },
"name": "GitBranch01" "name": "GitBranch01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/PromptEngineering.json View File

] ]
}, },
"name": "PromptEngineering" "name": "PromptEngineering"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/PuzzlePiece01.json View File

] ]
}, },
"name": "PuzzlePiece01" "name": "PuzzlePiece01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/TerminalSquare.json View File

] ]
}, },
"name": "TerminalSquare" "name": "TerminalSquare"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/Variable.json View File

] ]
}, },
"name": "Variable" "name": "Variable"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/Webhooks.json View File

] ]
}, },
"name": "Webhooks" "name": "Webhooks"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/AlignLeft.json View File

] ]
}, },
"name": "AlignLeft" "name": "AlignLeft"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/BezierCurve03.json View File

] ]
}, },
"name": "BezierCurve03" "name": "BezierCurve03"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/Collapse.json View File

] ]
}, },
"name": "Collapse" "name": "Collapse"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/Colors.json View File

] ]
}, },
"name": "Colors" "name": "Colors"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/ImageIndentLeft.json View File

] ]
}, },
"name": "ImageIndentLeft" "name": "ImageIndentLeft"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/LeftIndent02.json View File

] ]
}, },
"name": "LeftIndent02" "name": "LeftIndent02"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/LetterSpacing01.json View File

] ]
}, },
"name": "LetterSpacing01" "name": "LetterSpacing01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/TypeSquare.json View File

] ]
}, },
"name": "TypeSquare" "name": "TypeSquare"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/education/BookOpen01.json View File

] ]
}, },
"name": "BookOpen01" "name": "BookOpen01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/File02.json View File

] ]
}, },
"name": "File02" "name": "File02"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FileArrow01.json View File

] ]
}, },
"name": "FileArrow01" "name": "FileArrow01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FileCheck02.json View File

] ]
}, },
"name": "FileCheck02" "name": "FileCheck02"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FileDownload02.json View File

] ]
}, },
"name": "FileDownload02" "name": "FileDownload02"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FilePlus01.json View File

] ]
}, },
"name": "FilePlus01" "name": "FilePlus01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FilePlus02.json View File

] ]
}, },
"name": "FilePlus02" "name": "FilePlus02"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FileText.json View File

] ]
}, },
"name": "FileText" "name": "FileText"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FileUpload.json View File

] ]
}, },
"name": "FileUpload" "name": "FileUpload"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/Folder.json View File

] ]
}, },
"name": "Folder" "name": "Folder"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json View File

] ]
}, },
"name": "Balance" "name": "Balance"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/CoinsStacked01.json View File

] ]
}, },
"name": "CoinsStacked01" "name": "CoinsStacked01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.json View File

] ]
}, },
"name": "GoldCoin" "name": "GoldCoin"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.json View File

] ]
}, },
"name": "ReceiptList" "name": "ReceiptList"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag01.json View File

] ]
}, },
"name": "Tag01" "name": "Tag01"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag03.json View File

] ]
}, },
"name": "Tag03" "name": "Tag03"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/general/AtSign.json View File

] ]
}, },
"name": "AtSign" "name": "AtSign"
}
}

+ 1
- 1
web/app/components/base/icons/src/vender/line/general/Bookmark.json View File

] ]
}, },
"name": "Bookmark" "name": "Bookmark"
}
}

+ 0
- 0
web/app/components/base/icons/src/vender/line/general/Check.json View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save