mirror of https://github.com/langgenius/dify.git
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>
This commit is contained in:
parent
e6913744ae
commit
eaae79a581
|
|
@ -471,6 +471,16 @@ APP_MAX_ACTIVE_REQUESTS=0
|
|||
# Celery beat configuration
|
||||
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_TOOL_PINS=
|
||||
POSITION_TOOL_INCLUDES=
|
||||
|
|
|
|||
|
|
@ -74,7 +74,12 @@
|
|||
10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service.
|
||||
|
||||
```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
|
||||
|
|
|
|||
|
|
@ -832,6 +832,41 @@ class CeleryBeatConfig(BaseSettings):
|
|||
)
|
||||
|
||||
|
||||
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):
|
||||
POSITION_PROVIDER_PINS: str = Field(
|
||||
description="Comma-separated list of pinned model providers",
|
||||
|
|
@ -961,5 +996,6 @@ class FeatureConfig(
|
|||
# hosted services config
|
||||
HostedServiceConfig,
|
||||
CeleryBeatConfig,
|
||||
CeleryScheduleTasksConfig,
|
||||
):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ from controllers.console.wraps import account_initialization_required, setup_req
|
|||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
from core.plugin.impl.exc import PluginDaemonClientSideError
|
||||
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_permission_service import PluginPermissionService
|
||||
from services.plugin.plugin_service import PluginService
|
||||
|
|
@ -534,6 +535,114 @@ class PluginFetchDynamicSelectOptionsApi(Resource):
|
|||
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(PluginListApi, "/workspaces/current/plugin/list")
|
||||
api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions")
|
||||
|
|
@ -560,3 +669,7 @@ api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permissi
|
|||
api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch")
|
||||
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -25,9 +25,29 @@ def batch_fetch_plugin_manifests(plugin_ids: list[str]) -> Sequence[MarketplaceP
|
|||
url = str(marketplace_api_url / "api/v1/plugins/batch")
|
||||
response = requests.post(url, json={"plugin_ids": plugin_ids})
|
||||
response.raise_for_status()
|
||||
|
||||
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):
|
||||
url = str(marketplace_api_url / "api/v1/stats/plugins/install_count")
|
||||
response = requests.post(url, json={"unique_identifier": plugin_unique_identifier})
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ if [[ "${MODE}" == "worker" ]]; then
|
|||
|
||||
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} \
|
||||
-Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion}
|
||||
-Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion,plugin}
|
||||
|
||||
elif [[ "${MODE}" == "beat" ]]; then
|
||||
exec celery -A app.celery beat --loglevel ${LOG_LEVEL:-INFO}
|
||||
|
|
|
|||
|
|
@ -64,49 +64,62 @@ def init_app(app: DifyApp) -> Celery:
|
|||
celery_app.set_default()
|
||||
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
|
||||
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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"schedule": timedelta(
|
||||
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)
|
||||
|
||||
return celery_app
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
"""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 ###
|
||||
|
|
@ -297,6 +297,40 @@ class TenantPluginPermission(Base):
|
|||
)
|
||||
|
||||
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())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
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",
|
||||
)
|
||||
)
|
||||
|
|
@ -29,6 +29,7 @@ from models.account import (
|
|||
Tenant,
|
||||
TenantAccountJoin,
|
||||
TenantAccountRole,
|
||||
TenantPluginAutoUpgradeStrategy,
|
||||
TenantStatus,
|
||||
)
|
||||
from models.model import DifySetup
|
||||
|
|
@ -828,6 +829,17 @@ class TenantService:
|
|||
db.session.add(tenant)
|
||||
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)
|
||||
db.session.commit()
|
||||
return tenant
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
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
|
||||
|
|
@ -1168,3 +1168,13 @@ QUEUE_MONITOR_THRESHOLD=200
|
|||
QUEUE_MONITOR_ALERT_EMAILS=
|
||||
# Monitor interval in minutes, default is 30 minutes
|
||||
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
|
||||
|
|
|
|||
|
|
@ -55,6 +55,25 @@ services:
|
|||
- ssrf_proxy_network
|
||||
- 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.
|
||||
web:
|
||||
image: langgenius/dify-web:1.6.0
|
||||
|
|
|
|||
|
|
@ -527,6 +527,14 @@ x-shared-env: &shared-api-worker-env
|
|||
QUEUE_MONITOR_THRESHOLD: ${QUEUE_MONITOR_THRESHOLD:-200}
|
||||
QUEUE_MONITOR_ALERT_EMAILS: ${QUEUE_MONITOR_ALERT_EMAILS:-}
|
||||
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:
|
||||
# API service
|
||||
|
|
@ -584,6 +592,25 @@ services:
|
|||
- ssrf_proxy_network
|
||||
- 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.
|
||||
web:
|
||||
image: langgenius/dify-web:1.6.0
|
||||
|
|
|
|||
|
|
@ -4,18 +4,21 @@ import cn from '@/utils/classnames'
|
|||
type OptionListItemProps = {
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
noAutoScroll?: boolean
|
||||
} & React.LiHTMLAttributes<HTMLLIElement>
|
||||
|
||||
const OptionListItem: FC<OptionListItemProps> = ({
|
||||
isSelected,
|
||||
onClick,
|
||||
noAutoScroll,
|
||||
children,
|
||||
}) => {
|
||||
const listItemRef = useRef<HTMLLIElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected)
|
||||
if (isSelected && !noAutoScroll)
|
||||
listItemRef.current?.scrollIntoView({ behavior: 'instant' })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Header = () => {
|
||||
type Props = {
|
||||
title?: string
|
||||
}
|
||||
const Header = ({
|
||||
title,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<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'>
|
||||
{t('time.title.pickTime')}
|
||||
{title || t('time.title.pickTime')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ const TimePicker = ({
|
|||
onChange,
|
||||
onClear,
|
||||
renderTrigger,
|
||||
title,
|
||||
minuteFilter,
|
||||
popupClassName,
|
||||
}: TimePickerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
|
@ -108,18 +111,7 @@ const TimePicker = ({
|
|||
const displayValue = value?.format(timeFormat) || ''
|
||||
const placeholderDate = isOpen && selectedTime ? selectedTime.format(timeFormat) : (placeholder || t('time.defaultPlaceholder'))
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
placement='bottom-end'
|
||||
>
|
||||
<PortalToFollowElemTrigger>
|
||||
{renderTrigger ? (renderTrigger()) : (
|
||||
<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'
|
||||
onClick={handleClickTrigger}
|
||||
>
|
||||
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'
|
||||
|
|
@ -127,6 +119,24 @@ const TimePicker = ({
|
|||
value={isOpen ? '' : displayValue}
|
||||
placeholder={placeholderDate}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
placement='bottom-end'
|
||||
>
|
||||
<PortalToFollowElemTrigger>
|
||||
{renderTrigger ? (renderTrigger({
|
||||
inputElem,
|
||||
onClick: handleClickTrigger,
|
||||
isOpen,
|
||||
})) : (
|
||||
<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'
|
||||
onClick={handleClickTrigger}
|
||||
>
|
||||
{inputElem}
|
||||
<RiTimeLine className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-quaternary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
|
|
@ -142,14 +152,15 @@ const TimePicker = ({
|
|||
</div>
|
||||
)}
|
||||
</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'>
|
||||
{/* Header */}
|
||||
<Header />
|
||||
<Header title={title} />
|
||||
|
||||
{/* Time Options */}
|
||||
<Options
|
||||
selectedTime={selectedTime}
|
||||
minuteFilter={minuteFilter}
|
||||
handleSelectHour={handleSelectHour}
|
||||
handleSelectMinute={handleSelectMinute}
|
||||
handleSelectPeriod={handleSelectPeriod}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import OptionListItem from '../common/option-list-item'
|
|||
|
||||
const Options: FC<TimeOptionsProps> = ({
|
||||
selectedTime,
|
||||
minuteFilter,
|
||||
handleSelectHour,
|
||||
handleSelectMinute,
|
||||
handleSelectPeriod,
|
||||
|
|
@ -33,7 +34,7 @@ const Options: FC<TimeOptionsProps> = ({
|
|||
{/* Minute */}
|
||||
<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
|
||||
return (
|
||||
<OptionListItem
|
||||
|
|
@ -57,6 +58,7 @@ const Options: FC<TimeOptionsProps> = ({
|
|||
key={period}
|
||||
isSelected={isSelected}
|
||||
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}
|
||||
</OptionListItem>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export type DatePickerProps = {
|
|||
onClear: () => void
|
||||
triggerWrapClassName?: string
|
||||
renderTrigger?: (props: TriggerProps) => React.ReactNode
|
||||
minuteFilter?: (minutes: string[]) => string[]
|
||||
popupZIndexClassname?: string
|
||||
}
|
||||
|
||||
|
|
@ -47,13 +48,21 @@ export type DatePickerFooterProps = {
|
|||
handleConfirmDate: () => void
|
||||
}
|
||||
|
||||
export type TriggerParams = {
|
||||
isOpen: boolean
|
||||
inputElem: React.ReactNode
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
}
|
||||
export type TimePickerProps = {
|
||||
value: Dayjs | undefined
|
||||
timezone?: string
|
||||
placeholder?: string
|
||||
onChange: (date: Dayjs | undefined) => void
|
||||
onClear: () => void
|
||||
renderTrigger?: () => React.ReactNode
|
||||
renderTrigger?: (props: TriggerParams) => React.ReactNode
|
||||
title?: string
|
||||
minuteFilter?: (minutes: string[]) => string[]
|
||||
popupClassName?: string
|
||||
}
|
||||
|
||||
export type TimePickerFooterProps = {
|
||||
|
|
@ -81,6 +90,7 @@ export type CalendarItemProps = {
|
|||
|
||||
export type TimeOptionsProps = {
|
||||
selectedTime: Dayjs | undefined
|
||||
minuteFilter?: (minutes: string[]) => string[]
|
||||
handleSelectHour: (hour: string) => void
|
||||
handleSelectMinute: (minute: string) => void
|
||||
handleSelectPeriod: (period: Period) => void
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import dayjs, { type Dayjs } from 'dayjs'
|
|||
import type { Day } from '../types'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import tz from '@/utils/timezone.json'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
|
@ -78,3 +79,14 @@ export const getHourIn12Hour = (date: Dayjs) => {
|
|||
export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => {
|
||||
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)}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 823 B |
|
|
@ -0,0 +1,4 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 772 B |
|
|
@ -75,7 +75,7 @@ Icon.displayName = '<%= svgName %>'
|
|||
export default Icon
|
||||
`.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`)
|
||||
|
||||
const indexingRender = template(`
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -4,12 +4,16 @@
|
|||
import * as React from 'react'
|
||||
import data from './AliyunIcon.json'
|
||||
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,
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'AliyunIcon'
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -4,12 +4,16 @@
|
|||
import * as React from 'react'
|
||||
import data from './AliyunIconBig.json'
|
||||
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,
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'AliyunIconBig'
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@
|
|||
import * as React from 'react'
|
||||
import data from './WeaveIcon.json'
|
||||
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,
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'WeaveIcon'
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@
|
|||
import * as React from 'react'
|
||||
import data from './WeaveIconBig.json'
|
||||
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,
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'WeaveIconBig'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export { default as AliyunIconBig } from './AliyunIconBig'
|
||||
export { default as AliyunIcon } from './AliyunIcon'
|
||||
export { default as ArizeIconBig } from './ArizeIconBig'
|
||||
export { default as ArizeIcon } from './ArizeIcon'
|
||||
export { default as LangfuseIconBig } from './LangfuseIconBig'
|
||||
|
|
@ -11,5 +13,3 @@ export { default as PhoenixIcon } from './PhoenixIcon'
|
|||
export { default as TracingIcon } from './TracingIcon'
|
||||
export { default as WeaveIconBig } from './WeaveIconBig'
|
||||
export { default as WeaveIcon } from './WeaveIcon'
|
||||
export { default as AliyunIconBig } from './AliyunIconBig'
|
||||
export { default as AliyunIcon } from './AliyunIcon'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "32",
|
||||
"height": "32",
|
||||
"viewBox": "0 0 32 32",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"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": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4.00488 16H6.67155",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4.00488 9.33334H8.00488",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4.00488 22.6667H8.00488",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M26 22L29.3333 25.3333",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "SearchMenu"
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './SearchMenu.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'SearchMenu'
|
||||
|
||||
export default Icon
|
||||
|
|
@ -19,6 +19,7 @@ export { default as Pin01 } from './Pin01'
|
|||
export { default as Pin02 } from './Pin02'
|
||||
export { default as Plus02 } from './Plus02'
|
||||
export { default as Refresh } from './Refresh'
|
||||
export { default as SearchMenu } from './SearchMenu'
|
||||
export { default as Settings01 } from './Settings01'
|
||||
export { default as Settings04 } from './Settings04'
|
||||
export { default as Target04 } from './Target04'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"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": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"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": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "AutoUpdateLine"
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './AutoUpdateLine.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'AutoUpdateLine'
|
||||
|
||||
export default Icon
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as AutoUpdateLine } from './AutoUpdateLine'
|
||||
|
|
@ -10,7 +10,7 @@ import type { ExposeRefs } from './install-multi'
|
|||
import InstallMulti from './install-multi'
|
||||
import { useInstallOrUpdate } from '@/service/use-plugins'
|
||||
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
|
||||
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission'
|
||||
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-reference-setting'
|
||||
import { useMittContextSelector } from '@/context/mitt-context'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ import { PluginAuth } from '@/app/components/plugins/plugin-auth'
|
|||
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
import DeprecationNotice from '../base/deprecation-notice'
|
||||
import { AutoUpdateLine } from '../../base/icons/src/vender/system'
|
||||
import { convertUTCDaySecondsToLocalSeconds, timeOfDayToDayjs } from '../reference-setting-modal/auto-update-setting/utils'
|
||||
import useReferenceSetting from '../plugin-page/use-reference-setting'
|
||||
import { AUTO_UPDATE_MODE } from '../reference-setting-modal/auto-update-setting/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
const i18nPrefix = 'plugin.action'
|
||||
|
||||
|
|
@ -55,6 +60,8 @@ const DetailHeader = ({
|
|||
onUpdate,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile: { timezone } } = useAppContext()
|
||||
|
||||
const { theme } = useTheme()
|
||||
const locale = useGetLanguage()
|
||||
const { locale: currentLocale } = useI18N()
|
||||
|
|
@ -112,8 +119,24 @@ const DetailHeader = ({
|
|||
setFalse: hideUpdateModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleUpdate = async () => {
|
||||
const { referenceSetting } = useReferenceSetting()
|
||||
const { auto_upgrade: autoUpgradeInfo } = referenceSetting || {}
|
||||
const isAutoUpgradeEnabled = useMemo(() => {
|
||||
if (!autoUpgradeInfo || !isFromMarketplace)
|
||||
return false
|
||||
if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.update_all)
|
||||
return true
|
||||
if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.partial && autoUpgradeInfo.include_plugins.includes(plugin_id))
|
||||
return true
|
||||
if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.exclude && !autoUpgradeInfo.exclude_plugins.includes(plugin_id))
|
||||
return true
|
||||
return false
|
||||
}, [autoUpgradeInfo, plugin_id, isFromMarketplace])
|
||||
|
||||
const [isDowngrade, setIsDowngrade] = useState(false)
|
||||
const handleUpdate = async (isDowngrade?: boolean) => {
|
||||
if (isFromMarketplace) {
|
||||
setIsDowngrade(!!isDowngrade)
|
||||
showUpdateModal()
|
||||
return
|
||||
}
|
||||
|
|
@ -180,9 +203,6 @@ const DetailHeader = ({
|
|||
}
|
||||
}, [showDeleting, installation_id, hideDeleting, hideDeleteConfirm, onUpdate, category, refreshModelProviders, invalidateAllToolProviders])
|
||||
|
||||
// #plugin TODO# used in apps
|
||||
// const usedInApps = 3
|
||||
|
||||
return (
|
||||
<div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}>
|
||||
<div className="flex">
|
||||
|
|
@ -201,7 +221,7 @@ const DetailHeader = ({
|
|||
currentVersion={version}
|
||||
onSelect={(state) => {
|
||||
setTargetVersion(state)
|
||||
handleUpdate()
|
||||
handleUpdate(state.isDowngrade)
|
||||
}}
|
||||
trigger={
|
||||
<Badge
|
||||
|
|
@ -221,6 +241,18 @@ const DetailHeader = ({
|
|||
/>
|
||||
}
|
||||
/>
|
||||
{/* Auto update info */}
|
||||
{isAutoUpgradeEnabled && (
|
||||
<Tooltip popupContent={t('plugin.autoUpdate.nextUpdateTime', { time: timeOfDayToDayjs(convertUTCDaySecondsToLocalSeconds(autoUpgradeInfo?.upgrade_time_of_day || 0, timezone!)).format('hh:mm A') })}>
|
||||
{/* add a a div to fix tooltip hover not show problem */}
|
||||
<div>
|
||||
<Badge className='mr-1 cursor-pointer px-1'>
|
||||
<AutoUpdateLine className='size-3' />
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{(hasNewVersion || isFromGitHub) && (
|
||||
<Button variant='secondary-accent' size='small' className='!h-5' onClick={() => {
|
||||
if (isFromMarketplace) {
|
||||
|
|
@ -324,6 +356,7 @@ const DetailHeader = ({
|
|||
{
|
||||
isShowUpdateModal && (
|
||||
<UpdateFromMarketplace
|
||||
pluginId={plugin_id}
|
||||
payload={{
|
||||
category: detail.declaration.category,
|
||||
originalPackageInfo: {
|
||||
|
|
@ -337,6 +370,7 @@ const DetailHeader = ({
|
|||
}}
|
||||
onCancel={hideUpdateModal}
|
||||
onSave={handleUpdatedFromMarketplace}
|
||||
isShowDowngradeWarningModal={isDowngrade && isAutoUpgradeEnabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ import {
|
|||
} from './context'
|
||||
import InstallPluginDropdown from './install-plugin-dropdown'
|
||||
import { useUploader } from './use-uploader'
|
||||
import usePermission from './use-permission'
|
||||
import useReferenceSetting from './use-reference-setting'
|
||||
import DebugInfo from './debug-info'
|
||||
import PluginTasks from './plugin-tasks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import TabSlider from '@/app/components/base/tab-slider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import PermissionSetModal from '@/app/components/plugins/permission-setting-modal/modal'
|
||||
import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal/modal'
|
||||
import InstallFromMarketplace from '../install-plugin/install-from-marketplace'
|
||||
import {
|
||||
useRouter,
|
||||
|
|
@ -121,16 +121,16 @@ const PluginPage = ({
|
|||
}, [packageId, bundleInfo])
|
||||
|
||||
const {
|
||||
referenceSetting,
|
||||
canManagement,
|
||||
canDebugger,
|
||||
canSetPermissions,
|
||||
permissions,
|
||||
setPermissions,
|
||||
} = usePermission()
|
||||
setReferenceSettings,
|
||||
} = useReferenceSetting()
|
||||
const [showPluginSettingModal, {
|
||||
setTrue: setShowPluginSettingModal,
|
||||
setFalse: setHidePluginSettingModal,
|
||||
}] = useBoolean()
|
||||
}] = useBoolean(false)
|
||||
const [currentFile, setCurrentFile] = useState<File | null>(null)
|
||||
const containerRef = usePluginPageContext(v => v.containerRef)
|
||||
const options = usePluginPageContext(v => v.options)
|
||||
|
|
@ -276,10 +276,10 @@ const PluginPage = ({
|
|||
}
|
||||
|
||||
{showPluginSettingModal && (
|
||||
<PermissionSetModal
|
||||
payload={permissions!}
|
||||
<ReferenceSettingModal
|
||||
payload={referenceSetting!}
|
||||
onHide={setHidePluginSettingModal}
|
||||
onSave={setPermissions}
|
||||
onSave={setReferenceSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { PermissionType } from '../types'
|
|||
import { useAppContext } from '@/context/app-context'
|
||||
import Toast from '../../base/toast'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useInvalidatePermissions, useMutationPermissions, usePermissions } from '@/service/use-plugins'
|
||||
import { useInvalidateReferenceSettings, useMutationReferenceSettings, useReferenceSettings } from '@/service/use-plugins'
|
||||
import { useMemo } from 'react'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
|
|
@ -19,14 +19,16 @@ const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean)
|
|||
return isAdmin
|
||||
}
|
||||
|
||||
const usePermission = () => {
|
||||
const useReferenceSetting = () => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext()
|
||||
const { data: permissions } = usePermissions()
|
||||
const invalidatePermissions = useInvalidatePermissions()
|
||||
const { mutate: updatePermission, isPending: isUpdatePending } = useMutationPermissions({
|
||||
const { data } = useReferenceSettings()
|
||||
// console.log(data)
|
||||
const { permission: permissions } = data || {}
|
||||
const invalidateReferenceSettings = useInvalidateReferenceSettings()
|
||||
const { mutate: updateReferenceSetting, isPending: isUpdatePending } = useMutationReferenceSettings({
|
||||
onSuccess: () => {
|
||||
invalidatePermissions()
|
||||
invalidateReferenceSettings()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
|
|
@ -36,18 +38,18 @@ const usePermission = () => {
|
|||
const isAdmin = isCurrentWorkspaceManager || isCurrentWorkspaceOwner
|
||||
|
||||
return {
|
||||
referenceSetting: data,
|
||||
setReferenceSettings: updateReferenceSetting,
|
||||
canManagement: hasPermission(permissions?.install_permission, isAdmin),
|
||||
canDebugger: hasPermission(permissions?.debug_permission, isAdmin),
|
||||
canSetPermissions: isAdmin,
|
||||
permissions,
|
||||
setPermissions: updatePermission,
|
||||
isUpdatePending,
|
||||
}
|
||||
}
|
||||
|
||||
export const useCanInstallPluginFromMarketplace = () => {
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const { canManagement } = usePermission()
|
||||
const { canManagement } = useReferenceSetting()
|
||||
|
||||
const canInstallPluginFromMarketplace = useMemo(() => {
|
||||
return enable_marketplace && canManagement
|
||||
|
|
@ -58,4 +60,4 @@ export const useCanInstallPluginFromMarketplace = () => {
|
|||
}
|
||||
}
|
||||
|
||||
export default usePermission
|
||||
export default useReferenceSetting
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import type { AutoUpdateConfig } from './types'
|
||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from './types'
|
||||
export const defaultValue: AutoUpdateConfig = {
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.disabled,
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: AUTO_UPDATE_MODE.update_all,
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY, type AutoUpdateConfig } from './types'
|
||||
import Label from '../label'
|
||||
import StrategyPicker from './strategy-picker'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import PluginsPicker from './plugins-picker'
|
||||
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, dayjsToTimeOfDay, timeOfDayToDayjs } from './utils'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types'
|
||||
import { RiTimeLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
|
||||
const i18nPrefix = 'plugin.autoUpdate'
|
||||
|
||||
type Props = {
|
||||
payload: AutoUpdateConfig
|
||||
onChange: (payload: AutoUpdateConfig) => void
|
||||
}
|
||||
|
||||
const SettingTimeZone: FC<{
|
||||
children?: React.ReactNode
|
||||
}> = ({
|
||||
children,
|
||||
}) => {
|
||||
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||
return (
|
||||
<span className='body-xs-regular cursor-pointer text-text-accent' onClick={() => setShowAccountSettingModal({ payload: 'language' })} >{children}</span>
|
||||
)
|
||||
}
|
||||
const AutoUpdateSetting: FC<Props> = ({
|
||||
payload,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile: { timezone } } = useAppContext()
|
||||
|
||||
const {
|
||||
strategy_setting,
|
||||
upgrade_time_of_day,
|
||||
upgrade_mode,
|
||||
exclude_plugins,
|
||||
include_plugins,
|
||||
} = payload
|
||||
|
||||
const minuteFilter = useCallback((minutes: string[]) => {
|
||||
return minutes.filter((m) => {
|
||||
const time = Number.parseInt(m, 10)
|
||||
return time % 15 === 0
|
||||
})
|
||||
}, [])
|
||||
const strategyDescription = useMemo(() => {
|
||||
switch (strategy_setting) {
|
||||
case AUTO_UPDATE_STRATEGY.fixOnly:
|
||||
return t(`${i18nPrefix}.strategy.fixOnly.selectedDescription`)
|
||||
case AUTO_UPDATE_STRATEGY.latest:
|
||||
return t(`${i18nPrefix}.strategy.latest.selectedDescription`)
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}, [strategy_setting, t])
|
||||
|
||||
const plugins = useMemo(() => {
|
||||
switch (upgrade_mode) {
|
||||
case AUTO_UPDATE_MODE.partial:
|
||||
return include_plugins
|
||||
case AUTO_UPDATE_MODE.exclude:
|
||||
return exclude_plugins
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}, [upgrade_mode, exclude_plugins, include_plugins])
|
||||
|
||||
const handlePluginsChange = useCallback((newPlugins: string[]) => {
|
||||
if (upgrade_mode === AUTO_UPDATE_MODE.partial) {
|
||||
onChange({
|
||||
...payload,
|
||||
include_plugins: newPlugins,
|
||||
})
|
||||
}
|
||||
else if (upgrade_mode === AUTO_UPDATE_MODE.exclude) {
|
||||
onChange({
|
||||
...payload,
|
||||
exclude_plugins: newPlugins,
|
||||
})
|
||||
}
|
||||
}, [payload, upgrade_mode, onChange])
|
||||
const handleChange = useCallback((key: keyof AutoUpdateConfig) => {
|
||||
return (value: AutoUpdateConfig[keyof AutoUpdateConfig]) => {
|
||||
onChange({
|
||||
...payload,
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
}, [payload, onChange])
|
||||
|
||||
const renderTimePickerTrigger = useCallback(({ inputElem, onClick, isOpen }: TriggerParams) => {
|
||||
return (
|
||||
<div
|
||||
className='group float-right flex h-8 w-[160px] cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal px-2 hover:bg-state-base-hover-alt'
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className='flex w-0 grow items-center gap-x-1'>
|
||||
<RiTimeLine className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-tertiary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
)} />
|
||||
{inputElem}
|
||||
</div>
|
||||
<div className='system-sm-regular text-text-tertiary'>{convertTimezoneToOffsetStr(timezone)}</div>
|
||||
</div>
|
||||
)
|
||||
}, [timezone])
|
||||
|
||||
return (
|
||||
<div className='self-stretch px-6'>
|
||||
<div className='my-3 flex items-center'>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.updateSettings`)}</div>
|
||||
<div className='ml-2 h-px grow bg-divider-subtle'></div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label label={t(`${i18nPrefix}.automaticUpdates`)} description={strategyDescription} />
|
||||
<StrategyPicker value={strategy_setting} onChange={handleChange('strategy_setting')} />
|
||||
</div>
|
||||
{strategy_setting !== AUTO_UPDATE_STRATEGY.disabled && (
|
||||
<>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label label={t(`${i18nPrefix}.updateTime`)} />
|
||||
<div className='flex flex-col justify-start'>
|
||||
<TimePicker
|
||||
value={timeOfDayToDayjs(convertUTCDaySecondsToLocalSeconds(upgrade_time_of_day, timezone!))}
|
||||
timezone={timezone}
|
||||
onChange={v => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(dayjsToTimeOfDay(v), timezone!))}
|
||||
onClear={() => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(0, timezone!))}
|
||||
popupClassName='z-[99]'
|
||||
title={t(`${i18nPrefix}.updateTime`)}
|
||||
minuteFilter={minuteFilter}
|
||||
renderTrigger={renderTimePickerTrigger}
|
||||
/>
|
||||
<div className='body-xs-regular mt-1 text-right text-text-tertiary'>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.changeTimezone`}
|
||||
components={{
|
||||
setTimezone: <SettingTimeZone />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label label={t(`${i18nPrefix}.specifyPluginsToUpdate`)} />
|
||||
<div className='mt-1 flex w-full items-start justify-between gap-2'>
|
||||
{[AUTO_UPDATE_MODE.update_all, AUTO_UPDATE_MODE.exclude, AUTO_UPDATE_MODE.partial].map(option => (
|
||||
<OptionCard
|
||||
key={option}
|
||||
title={t(`${i18nPrefix}.upgradeMode.${option}`)}
|
||||
onSelect={() => handleChange('upgrade_mode')(option)}
|
||||
selected={upgrade_mode === option}
|
||||
className="flex-1"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{upgrade_mode !== AUTO_UPDATE_MODE.update_all && (
|
||||
<PluginsPicker
|
||||
value={plugins}
|
||||
onChange={handlePluginsChange}
|
||||
updateMode={upgrade_mode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AutoUpdateSetting)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
className: string
|
||||
noPlugins?: boolean
|
||||
}
|
||||
|
||||
const NoDataPlaceholder: FC<Props> = ({
|
||||
className,
|
||||
noPlugins,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const icon = noPlugins ? (<Group className='size-6 text-text-quaternary' />) : (<SearchMenu className='size-8 text-text-tertiary' />)
|
||||
const text = t(`plugin.autoUpdate.noPluginPlaceholder.${noPlugins ? 'noInstalled' : 'noFound'}`)
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center', className)}>
|
||||
<div className='flex flex-col items-center'>
|
||||
{icon}
|
||||
<div className='system-sm-regular mt-2 text-text-tertiary'>{text}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(NoDataPlaceholder)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { AUTO_UPDATE_MODE } from './types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
updateMode: AUTO_UPDATE_MODE
|
||||
}
|
||||
|
||||
const NoPluginSelected: FC<Props> = ({
|
||||
updateMode,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const text = `${t(`plugin.autoUpdate.upgradeModePlaceholder.${updateMode === AUTO_UPDATE_MODE.partial ? 'partial' : 'exclude'}`)}`
|
||||
return (
|
||||
<div className='system-xs-regular rounded-[10px] border border-[divider-subtle] bg-background-section p-3 text-center text-text-tertiary'>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(NoPluginSelected)
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import NoPluginSelected from './no-plugin-selected'
|
||||
import { AUTO_UPDATE_MODE } from './types'
|
||||
import PluginsSelected from './plugins-selected'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import ToolPicker from './tool-picker'
|
||||
|
||||
const i18nPrefix = 'plugin.autoUpdate'
|
||||
|
||||
type Props = {
|
||||
updateMode: AUTO_UPDATE_MODE
|
||||
value: string[] // plugin ids
|
||||
onChange: (value: string[]) => void
|
||||
}
|
||||
|
||||
const PluginsPicker: FC<Props> = ({
|
||||
updateMode,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const hasSelected = value.length > 0
|
||||
const isExcludeMode = updateMode === AUTO_UPDATE_MODE.exclude
|
||||
const handleClear = () => {
|
||||
onChange([])
|
||||
}
|
||||
|
||||
const [isShowToolPicker, {
|
||||
set: setToolPicker,
|
||||
}] = useBoolean(false)
|
||||
return (
|
||||
<div className='mt-2 rounded-[10px] bg-background-section-burn p-2.5'>
|
||||
{hasSelected ? (
|
||||
<div className='flex justify-between text-text-tertiary'>
|
||||
<div className='system-xs-medium'>{t(`${i18nPrefix}.${isExcludeMode ? 'excludeUpdate' : 'partialUPdate'}`, { num: value.length })}</div>
|
||||
<div className='system-xs-medium cursor-pointer' onClick={handleClear}>{t(`${i18nPrefix}.operation.clearAll`)}</div>
|
||||
</div>
|
||||
) : (
|
||||
<NoPluginSelected updateMode={updateMode} />
|
||||
)}
|
||||
|
||||
{hasSelected && (
|
||||
<PluginsSelected
|
||||
className='mt-2'
|
||||
plugins={value}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ToolPicker
|
||||
trigger={
|
||||
<Button className='mt-2 w-[412px]' size='small' variant='secondary-accent'>
|
||||
<RiAddLine className='size-3.5' />
|
||||
{t(`${i18nPrefix}.operation.select`)}
|
||||
</Button>
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isShow={isShowToolPicker}
|
||||
onShowChange={setToolPicker}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PluginsPicker)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
|
||||
const MAX_DISPLAY_COUNT = 14
|
||||
type Props = {
|
||||
className?: string
|
||||
plugins: string[]
|
||||
}
|
||||
|
||||
const PluginsSelected: FC<Props> = ({
|
||||
className,
|
||||
plugins,
|
||||
}) => {
|
||||
const isShowAll = plugins.length < MAX_DISPLAY_COUNT
|
||||
const displayPlugins = plugins.slice(0, MAX_DISPLAY_COUNT)
|
||||
return (
|
||||
<div className={cn('flex items-center space-x-1', className)}>
|
||||
{displayPlugins.map(plugin => (
|
||||
<Icon key={plugin} size='tiny' src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin}/icon`} />
|
||||
))}
|
||||
{!isShowAll && <div className='system-xs-medium text-text-tertiary'>+{plugins.length - MAX_DISPLAY_COUNT}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PluginsSelected)
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiCheckLine,
|
||||
} from '@remixicon/react'
|
||||
import { AUTO_UPDATE_STRATEGY } from './types'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
const i18nPrefix = 'plugin.autoUpdate.strategy'
|
||||
|
||||
type Props = {
|
||||
value: AUTO_UPDATE_STRATEGY
|
||||
onChange: (value: AUTO_UPDATE_STRATEGY) => void
|
||||
}
|
||||
const StrategyPicker = ({
|
||||
value,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const options = [
|
||||
{
|
||||
value: AUTO_UPDATE_STRATEGY.disabled,
|
||||
label: t(`${i18nPrefix}.disabled.name`),
|
||||
description: t(`${i18nPrefix}.disabled.description`),
|
||||
},
|
||||
{
|
||||
value: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
label: t(`${i18nPrefix}.fixOnly.name`),
|
||||
description: t(`${i18nPrefix}.fixOnly.description`),
|
||||
},
|
||||
{
|
||||
value: AUTO_UPDATE_STRATEGY.latest,
|
||||
label: t(`${i18nPrefix}.latest.name`),
|
||||
description: t(`${i18nPrefix}.latest.description`),
|
||||
},
|
||||
]
|
||||
const selectedOption = options.find(option => option.value === value)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='top-end'
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
setOpen(v => !v)
|
||||
}}>
|
||||
<Button
|
||||
size='small'
|
||||
>
|
||||
{selectedOption?.label}
|
||||
<RiArrowDownSLine className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[99]'>
|
||||
<div className='w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className='flex cursor-pointer rounded-lg p-2 pr-3 hover:bg-state-base-hover'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onChange(option.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className='mr-1 w-4 shrink-0'>
|
||||
{
|
||||
value === option.value && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='system-sm-semibold mb-0.5 text-text-secondary'>{option.label}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{option.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default StrategyPicker
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import { renderI18nObject } from '@/i18n'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
|
||||
type Props = {
|
||||
payload: PluginDetail
|
||||
isChecked?: boolean
|
||||
onCheckChange: () => void
|
||||
}
|
||||
|
||||
const ToolItem: FC<Props> = ({
|
||||
payload,
|
||||
isChecked,
|
||||
onCheckChange,
|
||||
}) => {
|
||||
const language = useGetLanguage()
|
||||
|
||||
const { plugin_id, declaration } = payload
|
||||
const { label, author: org } = declaration
|
||||
return (
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex w-full select-none items-center rounded-lg pr-2 hover:bg-state-base-hover'
|
||||
>
|
||||
<div className='flex h-8 grow items-center space-x-2 pl-3 pr-2'>
|
||||
<Icon size='tiny' src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin_id}/icon`} />
|
||||
<div className='system-sm-medium max-w-[150px] shrink-0 truncate text-text-primary'>{renderI18nObject(label, language)}</div>
|
||||
<div className='system-xs-regular max-w-[150px] shrink-0 truncate text-text-quaternary'>{org}</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
onCheck={onCheckChange}
|
||||
className='shrink-0'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ToolItem)
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useInstalledPluginList } from '@/service/use-plugins'
|
||||
import { PLUGIN_TYPE_SEARCH_MAP } from '../../marketplace/plugin-type-switch'
|
||||
import SearchBox from '@/app/components/plugins/marketplace/search-box'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import ToolItem from './tool-item'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import NoDataPlaceholder from './no-data-placeholder'
|
||||
import { PluginSource } from '../../types'
|
||||
|
||||
type Props = {
|
||||
trigger: React.ReactNode
|
||||
value: string[]
|
||||
onChange: (value: string[]) => void
|
||||
isShow: boolean
|
||||
onShowChange: (isShow: boolean) => void
|
||||
|
||||
}
|
||||
|
||||
const ToolPicker: FC<Props> = ({
|
||||
trigger,
|
||||
value,
|
||||
onChange,
|
||||
isShow,
|
||||
onShowChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const toggleShowPopup = useCallback(() => {
|
||||
onShowChange(!isShow)
|
||||
}, [onShowChange, isShow])
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.all,
|
||||
name: t('plugin.category.all'),
|
||||
},
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.model,
|
||||
name: t('plugin.category.models'),
|
||||
},
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.tool,
|
||||
name: t('plugin.category.tools'),
|
||||
},
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.agent,
|
||||
name: t('plugin.category.agents'),
|
||||
},
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.extension,
|
||||
name: t('plugin.category.extensions'),
|
||||
},
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.bundle,
|
||||
name: t('plugin.category.bundles'),
|
||||
},
|
||||
]
|
||||
|
||||
const [pluginType, setPluginType] = useState(PLUGIN_TYPE_SEARCH_MAP.all)
|
||||
const [query, setQuery] = useState('')
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
const { data, isLoading } = useInstalledPluginList()
|
||||
const filteredList = useMemo(() => {
|
||||
const list = data ? data.plugins : []
|
||||
return list.filter((plugin) => {
|
||||
const isFromMarketPlace = plugin.source === PluginSource.marketplace
|
||||
return (
|
||||
isFromMarketPlace && (pluginType === PLUGIN_TYPE_SEARCH_MAP.all || plugin.declaration.category === pluginType)
|
||||
&& (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag)))
|
||||
&& (query === '' || plugin.plugin_id.toLowerCase().includes(query.toLowerCase()))
|
||||
)
|
||||
})
|
||||
}, [data, pluginType, query, tags])
|
||||
const handleCheckChange = useCallback((pluginId: string) => {
|
||||
return () => {
|
||||
const newValue = value.includes(pluginId)
|
||||
? value.filter(id => id !== pluginId)
|
||||
: [...value, pluginId]
|
||||
onChange(newValue)
|
||||
}
|
||||
}, [onChange, value])
|
||||
|
||||
const listContent = (
|
||||
<div className='max-h-[396px] overflow-y-auto'>
|
||||
{filteredList.map(item => (
|
||||
<ToolItem
|
||||
key={item.plugin_id}
|
||||
payload={item}
|
||||
isChecked={value.includes(item.plugin_id)}
|
||||
onCheckChange={handleCheckChange(item.plugin_id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
const loadingContent = (
|
||||
<div className='flex h-[396px] items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
|
||||
const noData = (
|
||||
<NoDataPlaceholder className='h-[396px]' noPlugins={!query} />
|
||||
)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='top'
|
||||
offset={0}
|
||||
open={isShow}
|
||||
onOpenChange={onShowChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={toggleShowPopup}
|
||||
>
|
||||
{trigger}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className={cn('relative min-h-20 w-[436px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-2 shadow-lg backdrop-blur-sm')}>
|
||||
<div className='p-2 pb-1'>
|
||||
<SearchBox
|
||||
search={query}
|
||||
onSearchChange={setQuery}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
size='small'
|
||||
placeholder={t('plugin.searchTools')!}
|
||||
inputClassName='w-full'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-between border-b-[0.5px] border-divider-subtle bg-background-default-hover px-3 shadow-xs'>
|
||||
<div className='flex h-8 items-center space-x-1'>
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-6 cursor-pointer items-center rounded-md px-2 hover:bg-state-base-hover',
|
||||
'text-xs font-medium text-text-secondary',
|
||||
pluginType === tab.key && 'bg-state-base-hover-alt',
|
||||
)}
|
||||
key={tab.key}
|
||||
onClick={() => setPluginType(tab.key)}
|
||||
>
|
||||
{tab.name}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && filteredList.length > 0 && listContent}
|
||||
{!isLoading && filteredList.length === 0 && noData}
|
||||
{isLoading && loadingContent}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ToolPicker)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
export enum AUTO_UPDATE_STRATEGY {
|
||||
fixOnly = 'fix_only',
|
||||
disabled = 'disabled',
|
||||
latest = 'latest',
|
||||
}
|
||||
|
||||
export enum AUTO_UPDATE_MODE {
|
||||
partial = 'partial',
|
||||
exclude = 'exclude',
|
||||
update_all = 'all',
|
||||
}
|
||||
|
||||
export type AutoUpdateConfig = {
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY
|
||||
upgrade_time_of_day: number
|
||||
upgrade_mode: AUTO_UPDATE_MODE
|
||||
exclude_plugins: string[]
|
||||
include_plugins: string[]
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds } from './utils'
|
||||
|
||||
describe('convertLocalSecondsToUTCDaySeconds', () => {
|
||||
it('should convert local seconds to UTC day seconds correctly', () => {
|
||||
const localTimezone = 'Asia/Shanghai'
|
||||
const utcSeconds = convertLocalSecondsToUTCDaySeconds(0, localTimezone)
|
||||
expect(utcSeconds).toBe((24 - 8) * 3600)
|
||||
})
|
||||
|
||||
it('should convert local seconds to UTC day seconds for a specific time', () => {
|
||||
const localTimezone = 'Asia/Shanghai'
|
||||
expect(convertUTCDaySecondsToLocalSeconds(convertLocalSecondsToUTCDaySeconds(0, localTimezone), localTimezone)).toBe(0)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
export const timeOfDayToDayjs = (timeOfDay: number): Dayjs => {
|
||||
const hours = Math.floor(timeOfDay / 3600)
|
||||
const minutes = (timeOfDay - hours * 3600) / 60
|
||||
const res = dayjs().startOf('day').hour(hours).minute(minutes)
|
||||
return res
|
||||
}
|
||||
|
||||
export const convertLocalSecondsToUTCDaySeconds = (secondsInDay: number, localTimezone: string): number => {
|
||||
const localDayStart = dayjs().tz(localTimezone).startOf('day')
|
||||
const localTargetTime = localDayStart.add(secondsInDay, 'second')
|
||||
const utcTargetTime = localTargetTime.utc()
|
||||
const utcDayStart = utcTargetTime.startOf('day')
|
||||
const secondsFromUTCMidnight = utcTargetTime.diff(utcDayStart, 'second')
|
||||
return secondsFromUTCMidnight
|
||||
}
|
||||
|
||||
export const dayjsToTimeOfDay = (date?: Dayjs): number => {
|
||||
if (!date) return 0
|
||||
return date.hour() * 3600 + date.minute() * 60
|
||||
}
|
||||
|
||||
export const convertUTCDaySecondsToLocalSeconds = (utcDaySeconds: number, localTimezone: string): number => {
|
||||
const utcDayStart = dayjs().utc().startOf('day')
|
||||
const utcTargetTime = utcDayStart.add(utcDaySeconds, 'second')
|
||||
const localTargetTime = utcTargetTime.tz(localTimezone)
|
||||
const localDayStart = localTargetTime.startOf('day')
|
||||
const secondsInLocalDay = localTargetTime.diff(localDayStart, 'second')
|
||||
return secondsInLocalDay
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
label: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
const Label: FC<Props> = ({
|
||||
label,
|
||||
description,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className={cn('flex h-6 items-center', description && 'h-4')}>
|
||||
<span className='system-sm-semibold text-text-secondary'>{label}</span>
|
||||
</div>
|
||||
{description && (
|
||||
<div className='body-xs-regular mt-1 text-text-tertiary'>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Label)
|
||||
|
|
@ -5,14 +5,18 @@ import { useTranslation } from 'react-i18next'
|
|||
import Modal from '@/app/components/base/modal'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { Permissions } from '@/app/components/plugins/types'
|
||||
import type { Permissions, ReferenceSetting } from '@/app/components/plugins/types'
|
||||
import { PermissionType } from '@/app/components/plugins/types'
|
||||
import type { AutoUpdateConfig } from './auto-update-setting/types'
|
||||
import AutoUpdateSetting from './auto-update-setting'
|
||||
import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/config'
|
||||
import Label from './label'
|
||||
|
||||
const i18nPrefix = 'plugin.privilege'
|
||||
type Props = {
|
||||
payload: Permissions
|
||||
payload: ReferenceSetting
|
||||
onHide: () => void
|
||||
onSave: (payload: Permissions) => void
|
||||
onSave: (payload: ReferenceSetting) => void
|
||||
}
|
||||
|
||||
const PluginSettingModal: FC<Props> = ({
|
||||
|
|
@ -21,7 +25,9 @@ const PluginSettingModal: FC<Props> = ({
|
|||
onSave,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [tempPrivilege, setTempPrivilege] = useState<Permissions>(payload)
|
||||
const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {}
|
||||
const [tempPrivilege, setTempPrivilege] = useState<Permissions>(privilege)
|
||||
const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(autoUpdateConfig || autoUpdateDefaultValue)
|
||||
const handlePrivilegeChange = useCallback((key: string) => {
|
||||
return (value: PermissionType) => {
|
||||
setTempPrivilege({
|
||||
|
|
@ -32,18 +38,21 @@ const PluginSettingModal: FC<Props> = ({
|
|||
}, [tempPrivilege])
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
await onSave(tempPrivilege)
|
||||
await onSave({
|
||||
permission: tempPrivilege,
|
||||
auto_upgrade: tempAutoUpdateConfig,
|
||||
})
|
||||
onHide()
|
||||
}, [onHide, onSave, tempPrivilege])
|
||||
}, [onHide, onSave, tempAutoUpdateConfig, tempPrivilege])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onHide}
|
||||
closable
|
||||
className='w-[420px] !p-0'
|
||||
className='w-[480px] !p-0'
|
||||
>
|
||||
<div className='shadows-shadow-xl flex w-[420px] flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg'>
|
||||
<div className='shadows-shadow-xl flex w-[480px] flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg'>
|
||||
<div className='flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6'>
|
||||
<span className='title-2xl-semi-bold self-stretch text-text-primary'>{t(`${i18nPrefix}.title`)}</span>
|
||||
</div>
|
||||
|
|
@ -53,9 +62,7 @@ const PluginSettingModal: FC<Props> = ({
|
|||
{ title: t(`${i18nPrefix}.whoCanDebug`), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne },
|
||||
].map(({ title, key, value }) => (
|
||||
<div key={key} className='flex flex-col items-start gap-1 self-stretch'>
|
||||
<div className='flex h-6 items-center gap-0.5'>
|
||||
<span className='system-sm-semibold text-text-secondary'>{title}</span>
|
||||
</div>
|
||||
<Label label={title} />
|
||||
<div className='flex w-full items-start justify-between gap-2'>
|
||||
{[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => (
|
||||
<OptionCard
|
||||
|
|
@ -70,6 +77,8 @@ const PluginSettingModal: FC<Props> = ({
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
|
||||
<div className='flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5'>
|
||||
<Button
|
||||
className='min-w-[72px]'
|
||||
|
|
@ -2,6 +2,7 @@ import type { CredentialFormSchemaBase } from '../header/account-setting/model-p
|
|||
import type { ToolCredential } from '@/app/components/tools/types'
|
||||
import type { Locale } from '@/i18n'
|
||||
import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types'
|
||||
import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types'
|
||||
export enum PluginType {
|
||||
tool = 'tool',
|
||||
model = 'model',
|
||||
|
|
@ -170,6 +171,11 @@ export type Permissions = {
|
|||
debug_permission: PermissionType
|
||||
}
|
||||
|
||||
export type ReferenceSetting = {
|
||||
permission: Permissions
|
||||
auto_upgrade: AutoUpdateConfig
|
||||
}
|
||||
|
||||
export type UpdateFromMarketPlacePayload = {
|
||||
category: PluginType
|
||||
originalPackageInfo: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
const i18nPrefix = 'plugin.autoUpdate.pluginDowngradeWarning'
|
||||
|
||||
type Props = {
|
||||
onCancel: () => void
|
||||
onJustDowngrade: () => void
|
||||
onExcludeAndDowngrade: () => void
|
||||
}
|
||||
const DowngradeWarningModal = ({
|
||||
onCancel,
|
||||
onJustDowngrade,
|
||||
onExcludeAndDowngrade,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col items-start gap-2 self-stretch'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t(`${i18nPrefix}.title`)}</div>
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-9 flex items-start justify-end space-x-2 self-stretch'>
|
||||
<Button variant='secondary' onClick={() => onCancel()}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button variant='secondary' destructive onClick={onJustDowngrade}>{t(`${i18nPrefix}.downgrade`)}</Button>
|
||||
<Button variant='primary' onClick={onExcludeAndDowngrade}>{t(`${i18nPrefix}.exclude`)}</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DowngradeWarningModal
|
||||
|
|
@ -13,13 +13,18 @@ import { updateFromMarketPlace } from '@/service/plugins'
|
|||
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
|
||||
import { usePluginTaskList } from '@/service/use-plugins'
|
||||
import Toast from '../../base/toast'
|
||||
import DowngradeWarningModal from './downgrade-warning'
|
||||
import { useInvalidateReferenceSettings, useRemoveAutoUpgrade } from '@/service/use-plugins'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'plugin.upgrade'
|
||||
|
||||
type Props = {
|
||||
payload: UpdateFromMarketPlacePayload
|
||||
pluginId: string
|
||||
onSave: () => void
|
||||
onCancel: () => void
|
||||
isShowDowngradeWarningModal?: boolean
|
||||
}
|
||||
|
||||
enum UploadStep {
|
||||
|
|
@ -30,8 +35,10 @@ enum UploadStep {
|
|||
|
||||
const UpdatePluginModal: FC<Props> = ({
|
||||
payload,
|
||||
pluginId,
|
||||
onSave,
|
||||
onCancel,
|
||||
isShowDowngradeWarningModal,
|
||||
}) => {
|
||||
const {
|
||||
originalPackageInfo,
|
||||
|
|
@ -103,14 +110,34 @@ const UpdatePluginModal: FC<Props> = ({
|
|||
onSave()
|
||||
}, [onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id])
|
||||
|
||||
const { mutateAsync } = useRemoveAutoUpgrade()
|
||||
const invalidateReferenceSettings = useInvalidateReferenceSettings()
|
||||
const handleExcludeAndDownload = async () => {
|
||||
await mutateAsync({
|
||||
plugin_id: pluginId,
|
||||
})
|
||||
invalidateReferenceSettings()
|
||||
handleConfirm()
|
||||
}
|
||||
const doShowDowngradeWarningModal = isShowDowngradeWarningModal && uploadStep === UploadStep.notStarted
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={onCancel}
|
||||
className='min-w-[560px]'
|
||||
className={cn('min-w-[560px]', doShowDowngradeWarningModal && 'min-w-[640px]')}
|
||||
closable
|
||||
title={t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)}
|
||||
title={!doShowDowngradeWarningModal && t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)}
|
||||
>
|
||||
{doShowDowngradeWarningModal && (
|
||||
<DowngradeWarningModal
|
||||
onCancel={onCancel}
|
||||
onJustDowngrade={handleConfirm}
|
||||
onExcludeAndDowngrade={handleExcludeAndDownload}
|
||||
/>
|
||||
)}
|
||||
{!doShowDowngradeWarningModal && (
|
||||
<>
|
||||
<div className='system-md-regular mb-2 mt-3 text-text-secondary'>
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
</div>
|
||||
|
|
@ -148,6 +175,9 @@ const UpdatePluginModal: FC<Props> = ({
|
|||
{configBtnText}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type {
|
|||
import { useVersionListOfPlugin } from '@/service/use-plugins'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import cn from '@/utils/classnames'
|
||||
import { lt } from 'semver'
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean
|
||||
|
|
@ -28,9 +29,11 @@ type Props = {
|
|||
onSelect: ({
|
||||
version,
|
||||
unique_identifier,
|
||||
isDowngrade,
|
||||
}: {
|
||||
version: string
|
||||
unique_identifier: string
|
||||
isDowngrade: boolean
|
||||
}) => void
|
||||
}
|
||||
|
||||
|
|
@ -59,13 +62,14 @@ const PluginVersionPicker: FC<Props> = ({
|
|||
|
||||
const { data: res } = useVersionListOfPlugin(pluginID)
|
||||
|
||||
const handleSelect = useCallback(({ version, unique_identifier }: {
|
||||
const handleSelect = useCallback(({ version, unique_identifier, isDowngrade }: {
|
||||
version: string
|
||||
unique_identifier: string
|
||||
isDowngrade: boolean
|
||||
}) => {
|
||||
if (currentVersion === version)
|
||||
return
|
||||
onSelect({ version, unique_identifier })
|
||||
onSelect({ version, unique_identifier, isDowngrade })
|
||||
onShowChange(false)
|
||||
}, [currentVersion, onSelect, onShowChange])
|
||||
|
||||
|
|
@ -99,6 +103,7 @@ const PluginVersionPicker: FC<Props> = ({
|
|||
onClick={() => handleSelect({
|
||||
version: version.version,
|
||||
unique_identifier: version.unique_identifier,
|
||||
isDowngrade: lt(version.version, currentVersion),
|
||||
})}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
|
|
|
|||
|
|
@ -125,6 +125,56 @@ const translation = {
|
|||
admins: 'Admins',
|
||||
noone: 'No one',
|
||||
},
|
||||
autoUpdate: {
|
||||
automaticUpdates: 'Automatic updates',
|
||||
updateTime: 'Update time',
|
||||
specifyPluginsToUpdate: 'Specify plugins to update',
|
||||
strategy: {
|
||||
disabled: {
|
||||
name: 'Disabled',
|
||||
description: 'Plugins will not auto-update',
|
||||
},
|
||||
fixOnly: {
|
||||
name: 'Fix Only',
|
||||
description: 'Auto-update for patch versions only (e.g., 1.0.1 → 1.0.2). Minor version changes won\'t trigger updates.',
|
||||
selectedDescription: 'Auto-update for patch versions only',
|
||||
},
|
||||
latest: {
|
||||
name: 'Latest',
|
||||
description: 'Always update to latest version',
|
||||
selectedDescription: 'Always update to latest version',
|
||||
},
|
||||
},
|
||||
updateTimeTitle: 'Update time',
|
||||
upgradeMode: {
|
||||
all: 'Update all',
|
||||
exclude: 'Exclude selected',
|
||||
partial: 'Only selected',
|
||||
},
|
||||
upgradeModePlaceholder: {
|
||||
exclude: 'Selected plugins will not auto-update',
|
||||
partial: 'Only selected plugins will auto-update. No plugins are currently selected, so no plugins will auto-update.',
|
||||
},
|
||||
excludeUpdate: 'The following {{num}} plugins will not auto-update',
|
||||
partialUPdate: 'Only the following {{num}} plugins will auto-update',
|
||||
operation: {
|
||||
clearAll: 'Clear all',
|
||||
select: 'Select plugins',
|
||||
},
|
||||
nextUpdateTime: 'Next auto-update: {{time}}',
|
||||
pluginDowngradeWarning: {
|
||||
title: 'Plugin Downgrade',
|
||||
description: 'Auto-update is currently enabled for this plugin. Downgrading the version may cause your changes to be overwritten during the next automatic update.',
|
||||
downgrade: 'Downgrade anyway',
|
||||
exclude: 'Exclude from auto-update',
|
||||
},
|
||||
noPluginPlaceholder: {
|
||||
noFound: 'No plugins were found',
|
||||
noInstalled: 'No plugins installed',
|
||||
},
|
||||
updateSettings: 'Update Settings',
|
||||
changeTimezone: 'To change time zone, go to <setTimezone>Settings</setTimezone>',
|
||||
},
|
||||
pluginInfoModal: {
|
||||
title: 'Plugin info',
|
||||
repository: 'Repository',
|
||||
|
|
|
|||
|
|
@ -125,6 +125,56 @@ const translation = {
|
|||
admins: '管理员',
|
||||
noone: '无人',
|
||||
},
|
||||
autoUpdate: {
|
||||
automaticUpdates: '自动更新',
|
||||
updateTime: '更新时间',
|
||||
specifyPluginsToUpdate: '指定要更新的插件',
|
||||
strategy: {
|
||||
disabled: {
|
||||
name: '禁用',
|
||||
description: '插件将不会自动更新',
|
||||
},
|
||||
fixOnly: {
|
||||
name: '仅修复',
|
||||
description: '仅自动更新补丁版本(例如,1.0.1 → 1.0.2)。次要版本更改不会触发更新。',
|
||||
selectedDescription: '仅自动更新补丁版本',
|
||||
},
|
||||
latest: {
|
||||
name: '最新',
|
||||
description: '始终更新到最新版本',
|
||||
selectedDescription: '始终更新到最新版本',
|
||||
},
|
||||
},
|
||||
updateTimeTitle: '更新时间',
|
||||
upgradeMode: {
|
||||
all: '更新全部',
|
||||
exclude: '排除选定',
|
||||
partial: '仅选定',
|
||||
},
|
||||
upgradeModePlaceholder: {
|
||||
exclude: '选定的插件将不会自动更新',
|
||||
partial: '仅选定的插件将自动更新。目前未选择任何插件,因此不会自动更新任何插件。',
|
||||
},
|
||||
excludeUpdate: '以下 {{num}} 个插件将不会自动更新',
|
||||
partialUPdate: '仅以下 {{num}} 个插件将自动更新',
|
||||
operation: {
|
||||
clearAll: '清除所有',
|
||||
select: '选择插件',
|
||||
},
|
||||
nextUpdateTime: '下次自动更新时间: {{time}}',
|
||||
pluginDowngradeWarning: {
|
||||
title: '插件降级',
|
||||
description: '此插件目前已启用自动更新。降级版本可能会导致您的更改在下次自动更新时被覆盖。',
|
||||
downgrade: '仍然降级',
|
||||
exclude: '从自动更新中排除',
|
||||
},
|
||||
noPluginPlaceholder: {
|
||||
noFound: '未找到插件',
|
||||
noInstalled: '未安装插件',
|
||||
},
|
||||
updateSettings: '更新设置',
|
||||
changeTimezone: '要更改时区,请前往<setTimezone>设置</setTimezone>',
|
||||
},
|
||||
pluginInfoModal: {
|
||||
title: '插件信息',
|
||||
repository: '仓库',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import type {
|
|||
InstalledLatestVersionResponse,
|
||||
InstalledPluginListWithTotalResponse,
|
||||
PackageDependency,
|
||||
Permissions,
|
||||
Plugin,
|
||||
PluginDeclaration,
|
||||
PluginDetail,
|
||||
|
|
@ -22,6 +21,7 @@ import type {
|
|||
PluginType,
|
||||
PluginsFromMarketplaceByInfoResponse,
|
||||
PluginsFromMarketplaceResponse,
|
||||
ReferenceSetting,
|
||||
VersionInfo,
|
||||
VersionListResponse,
|
||||
uploadGitHubResponse,
|
||||
|
|
@ -40,7 +40,7 @@ import {
|
|||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { useInvalidateAllBuiltInTools } from './use-tools'
|
||||
import usePermission from '@/app/components/plugins/plugin-page/use-permission'
|
||||
import useReferenceSetting from '@/app/components/plugins/plugin-page/use-reference-setting'
|
||||
import { uninstallPlugin } from '@/service/plugins'
|
||||
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
|
@ -350,37 +350,45 @@ export const useDebugKey = () => {
|
|||
})
|
||||
}
|
||||
|
||||
const usePermissionsKey = [NAME_SPACE, 'permissions']
|
||||
export const usePermissions = () => {
|
||||
const useReferenceSettingKey = [NAME_SPACE, 'referenceSettings']
|
||||
export const useReferenceSettings = () => {
|
||||
return useQuery({
|
||||
queryKey: usePermissionsKey,
|
||||
queryFn: () => get<Permissions>('/workspaces/current/plugin/permission/fetch'),
|
||||
queryKey: useReferenceSettingKey,
|
||||
queryFn: () => get<ReferenceSetting>('/workspaces/current/plugin/preferences/fetch'),
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidatePermissions = () => {
|
||||
export const useInvalidateReferenceSettings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return () => {
|
||||
queryClient.invalidateQueries(
|
||||
{
|
||||
queryKey: usePermissionsKey,
|
||||
queryKey: useReferenceSettingKey,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const useMutationPermissions = ({
|
||||
export const useMutationReferenceSettings = ({
|
||||
onSuccess,
|
||||
}: {
|
||||
onSuccess?: () => void
|
||||
}) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: Permissions) => {
|
||||
return post('/workspaces/current/plugin/permission/change', { body: payload })
|
||||
mutationFn: (payload: ReferenceSetting) => {
|
||||
return post('/workspaces/current/plugin/preferences/change', { body: payload })
|
||||
},
|
||||
onSuccess,
|
||||
})
|
||||
}
|
||||
|
||||
export const useRemoveAutoUpgrade = () => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: { plugin_id: string }) => {
|
||||
return post('/workspaces/current/plugin/preferences/autoupgrade/exclude', { body: payload })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useMutationPluginsFromMarketplace = () => {
|
||||
return useMutation({
|
||||
mutationFn: (pluginsSearchParams: PluginsSearchParams) => {
|
||||
|
|
@ -427,6 +435,39 @@ export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[],
|
|||
})
|
||||
}
|
||||
|
||||
export const useFetchPluginListOrBundleList = (pluginsSearchParams: PluginsSearchParams) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'fetchPluginListOrBundleList', pluginsSearchParams],
|
||||
queryFn: () => {
|
||||
const {
|
||||
query,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
category,
|
||||
tags,
|
||||
exclude,
|
||||
type,
|
||||
page = 1,
|
||||
pageSize = 40,
|
||||
} = pluginsSearchParams
|
||||
const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
|
||||
return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
|
||||
body: {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
query,
|
||||
sort_by: sortBy,
|
||||
sort_order: sortOrder,
|
||||
category: category !== 'all' ? category : '',
|
||||
tags,
|
||||
exclude,
|
||||
type,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[]) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByInfo', infos],
|
||||
|
|
@ -448,7 +489,7 @@ const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList']
|
|||
export const usePluginTaskList = (category?: PluginType) => {
|
||||
const {
|
||||
canManagement,
|
||||
} = usePermission()
|
||||
} = useReferenceSetting()
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
const {
|
||||
data,
|
||||
|
|
|
|||
Loading…
Reference in New Issue