Unverified Commit 5bb84193 authored by zxhlyh's avatar zxhlyh Committed by GitHub

feat: custom webapp logo (#1766)

parent 65fd4b39
...@@ -10,12 +10,15 @@ from controllers.console import api ...@@ -10,12 +10,15 @@ from controllers.console import api
from controllers.console.admin import admin_required from controllers.console.admin import admin_required
from controllers.console.setup import setup_required from controllers.console.setup import setup_required
from controllers.console.error import AccountNotLinkTenantError from controllers.console.error import AccountNotLinkTenantError
from controllers.console.wraps import account_initialization_required from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
from controllers.console.datasets.error import NoFileUploadedError, TooManyFilesError, FileTooLargeError, UnsupportedFileTypeError
from libs.helper import TimestampField from libs.helper import TimestampField
from extensions.ext_database import db from extensions.ext_database import db
from models.account import Tenant from models.account import Tenant
import services
from services.account_service import TenantService from services.account_service import TenantService
from services.workspace_service import WorkspaceService from services.workspace_service import WorkspaceService
from services.file_service import FileService
provider_fields = { provider_fields = {
'provider_name': fields.String, 'provider_name': fields.String,
...@@ -34,6 +37,7 @@ tenant_fields = { ...@@ -34,6 +37,7 @@ tenant_fields = {
'providers': fields.List(fields.Nested(provider_fields)), 'providers': fields.List(fields.Nested(provider_fields)),
'in_trial': fields.Boolean, 'in_trial': fields.Boolean,
'trial_end_reason': fields.String, 'trial_end_reason': fields.String,
'custom_config': fields.Raw(attribute='custom_config'),
} }
tenants_fields = { tenants_fields = {
...@@ -130,6 +134,61 @@ class SwitchWorkspaceApi(Resource): ...@@ -130,6 +134,61 @@ class SwitchWorkspaceApi(Resource):
new_tenant = db.session.query(Tenant).get(args['tenant_id']) # Get new tenant new_tenant = db.session.query(Tenant).get(args['tenant_id']) # Get new tenant
return {'result': 'success', 'new_tenant': marshal(WorkspaceService.get_tenant_info(new_tenant), tenant_fields)} return {'result': 'success', 'new_tenant': marshal(WorkspaceService.get_tenant_info(new_tenant), tenant_fields)}
class CustomConfigWorkspaceApi(Resource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check('workspace_custom')
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('remove_webapp_brand', type=bool, location='json')
parser.add_argument('replace_webapp_logo', type=str, location='json')
args = parser.parse_args()
custom_config_dict = {
'remove_webapp_brand': args['remove_webapp_brand'],
'replace_webapp_logo': args['replace_webapp_logo'],
}
tenant = db.session.query(Tenant).filter(Tenant.id == current_user.current_tenant_id).one_or_404()
tenant.custom_config_dict = custom_config_dict
db.session.commit()
return {'result': 'success', 'tenant': marshal(WorkspaceService.get_tenant_info(tenant), tenant_fields)}
class WebappLogoWorkspaceApi(Resource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check('workspace_custom')
def post(self):
# get file from request
file = request.files['file']
# check file
if 'file' not in request.files:
raise NoFileUploadedError()
if len(request.files) > 1:
raise TooManyFilesError()
extension = file.filename.split('.')[-1]
if extension.lower() not in ['svg', 'png']:
raise UnsupportedFileTypeError()
try:
upload_file = FileService.upload_file(file, current_user, True)
except services.errors.file.FileTooLargeError as file_too_large_error:
raise FileTooLargeError(file_too_large_error.description)
except services.errors.file.UnsupportedFileTypeError:
raise UnsupportedFileTypeError()
return { 'id': upload_file.id }, 201
api.add_resource(TenantListApi, '/workspaces') # GET for getting all tenants api.add_resource(TenantListApi, '/workspaces') # GET for getting all tenants
...@@ -137,3 +196,5 @@ api.add_resource(WorkspaceListApi, '/all-workspaces') # GET for getting all ten ...@@ -137,3 +196,5 @@ api.add_resource(WorkspaceListApi, '/all-workspaces') # GET for getting all ten
api.add_resource(TenantApi, '/workspaces/current', endpoint='workspaces_current') # GET for getting current tenant info api.add_resource(TenantApi, '/workspaces/current', endpoint='workspaces_current') # GET for getting current tenant info
api.add_resource(TenantApi, '/info', endpoint='info') # Deprecated api.add_resource(TenantApi, '/info', endpoint='info') # Deprecated
api.add_resource(SwitchWorkspaceApi, '/workspaces/switch') # POST for switching tenant api.add_resource(SwitchWorkspaceApi, '/workspaces/switch') # POST for switching tenant
api.add_resource(CustomConfigWorkspaceApi, '/workspaces/custom-config')
api.add_resource(WebappLogoWorkspaceApi, '/workspaces/custom-config/webapp-logo/upload')
...@@ -63,6 +63,8 @@ def cloud_edition_billing_resource_check(resource: str, ...@@ -63,6 +63,8 @@ def cloud_edition_billing_resource_check(resource: str,
abort(403, error_msg) abort(403, error_msg)
elif resource == 'vector_space' and 0 < vector_space['limit'] <= vector_space['size']: elif resource == 'vector_space' and 0 < vector_space['limit'] <= vector_space['size']:
abort(403, error_msg) abort(403, error_msg)
elif resource == 'workspace_custom' and not billing_info['can_replace_logo']:
abort(403, error_msg)
elif resource == 'annotation' and 0 < annotation_quota_limit['limit'] <= annotation_quota_limit['size']: elif resource == 'annotation' and 0 < annotation_quota_limit['limit'] <= annotation_quota_limit['size']:
abort(403, error_msg) abort(403, error_msg)
else: else:
......
from flask import request, Response from flask import request, Response
from flask_restful import Resource from flask_restful import Resource
from werkzeug.exceptions import NotFound
import services import services
from controllers.files import api from controllers.files import api
from libs.exception import BaseHTTPException from libs.exception import BaseHTTPException
from services.file_service import FileService from services.file_service import FileService
from services.account_service import TenantService
class ImagePreviewApi(Resource): class ImagePreviewApi(Resource):
...@@ -29,9 +31,30 @@ class ImagePreviewApi(Resource): ...@@ -29,9 +31,30 @@ class ImagePreviewApi(Resource):
raise UnsupportedFileTypeError() raise UnsupportedFileTypeError()
return Response(generator, mimetype=mimetype) return Response(generator, mimetype=mimetype)
class WorkspaceWebappLogoApi(Resource):
def get(self, workspace_id):
workspace_id = str(workspace_id)
custom_config = TenantService.get_custom_config(workspace_id)
webapp_logo_file_id = custom_config.get('replace_webapp_logo') if custom_config is not None else None
if not webapp_logo_file_id:
raise NotFound(f'webapp logo is not found')
try:
generator, mimetype = FileService.get_public_image_preview(
webapp_logo_file_id,
)
except services.errors.file.UnsupportedFileTypeError:
raise UnsupportedFileTypeError()
return Response(generator, mimetype=mimetype)
api.add_resource(ImagePreviewApi, '/files/<uuid:file_id>/image-preview') api.add_resource(ImagePreviewApi, '/files/<uuid:file_id>/image-preview')
api.add_resource(WorkspaceWebappLogoApi, '/files/workspaces/<uuid:workspace_id>/webapp-logo')
class UnsupportedFileTypeError(BaseHTTPException): class UnsupportedFileTypeError(BaseHTTPException):
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import os import os
from flask_restful import fields, marshal_with from flask_restful import fields, marshal_with
from flask import current_app
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from controllers.web import api from controllers.web import api
...@@ -43,6 +44,7 @@ class AppSiteApi(WebApiResource): ...@@ -43,6 +44,7 @@ class AppSiteApi(WebApiResource):
'model_config': fields.Nested(model_config_fields, allow_null=True), 'model_config': fields.Nested(model_config_fields, allow_null=True),
'plan': fields.String, 'plan': fields.String,
'can_replace_logo': fields.Boolean, 'can_replace_logo': fields.Boolean,
'custom_config': fields.Raw(attribute='custom_config'),
} }
@marshal_with(app_fields) @marshal_with(app_fields)
...@@ -80,6 +82,15 @@ class AppSiteInfo: ...@@ -80,6 +82,15 @@ class AppSiteInfo:
self.plan = tenant.plan self.plan = tenant.plan
self.can_replace_logo = can_replace_logo self.can_replace_logo = can_replace_logo
if can_replace_logo:
base_url = current_app.config.get('FILES_URL')
remove_webapp_brand = tenant.custom_config_dict.get('remove_webapp_brand', False)
replace_webapp_logo = f'{base_url}/files/workspaces/{tenant.id}/webapp-logo' if tenant.custom_config_dict['replace_webapp_logo'] else None
self.custom_config = {
'remove_webapp_brand': remove_webapp_brand,
'replace_webapp_logo': replace_webapp_logo,
}
if app.enable_site and site.prompt_public: if app.enable_site and site.prompt_public:
app_model_config = app.app_model_config app_model_config = app.app_model_config
self.model_config = app_model_config self.model_config = app_model_config
...@@ -10,7 +10,7 @@ from flask import current_app ...@@ -10,7 +10,7 @@ from flask import current_app
from extensions.ext_storage import storage from extensions.ext_storage import storage
SUPPORT_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'] SUPPORT_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg']
class UploadFileParser: class UploadFileParser:
......
"""add custom config in tenant
Revision ID: 88072f0caa04
Revises: fca025d3b60f
Create Date: 2023-12-14 07:36:50.705362
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '88072f0caa04'
down_revision = '246ba09cbbdb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenants', schema=None) as batch_op:
batch_op.add_column(sa.Column('custom_config', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenants', schema=None) as batch_op:
batch_op.drop_column('custom_config')
# ### end Alembic commands ###
import json
import enum import enum
from math import e
from typing import List from typing import List
from flask_login import UserMixin from flask_login import UserMixin
...@@ -112,6 +114,7 @@ class Tenant(db.Model): ...@@ -112,6 +114,7 @@ class Tenant(db.Model):
encrypt_public_key = db.Column(db.Text) encrypt_public_key = db.Column(db.Text)
plan = db.Column(db.String(255), nullable=False, server_default=db.text("'basic'::character varying")) plan = db.Column(db.String(255), nullable=False, server_default=db.text("'basic'::character varying"))
status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying"))
custom_config = db.Column(db.Text)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
...@@ -121,6 +124,14 @@ class Tenant(db.Model): ...@@ -121,6 +124,14 @@ class Tenant(db.Model):
Account.id == TenantAccountJoin.account_id, Account.id == TenantAccountJoin.account_id,
TenantAccountJoin.tenant_id == self.id TenantAccountJoin.tenant_id == self.id
).all() ).all()
@property
def custom_config_dict(self) -> dict:
return json.loads(self.custom_config) if self.custom_config else None
@custom_config_dict.setter
def custom_config_dict(self, value: dict):
self.custom_config = json.dumps(value)
class TenantAccountJoinRole(enum.Enum): class TenantAccountJoinRole(enum.Enum):
......
...@@ -412,6 +412,12 @@ class TenantService: ...@@ -412,6 +412,12 @@ class TenantService:
db.session.delete(tenant) db.session.delete(tenant)
db.session.commit() db.session.commit()
@staticmethod
def get_custom_config(tenant_id: str) -> None:
tenant = db.session.query(Tenant).filter(Tenant.id == tenant_id).one_or_404()
return tenant.custom_config_dict
class RegisterService: class RegisterService:
......
...@@ -17,8 +17,8 @@ from models.model import UploadFile, EndUser ...@@ -17,8 +17,8 @@ from models.model import UploadFile, EndUser
from services.errors.file import FileTooLargeError, UnsupportedFileTypeError from services.errors.file import FileTooLargeError, UnsupportedFileTypeError
ALLOWED_EXTENSIONS = ['txt', 'markdown', 'md', 'pdf', 'html', 'htm', 'xlsx', 'docx', 'csv', ALLOWED_EXTENSIONS = ['txt', 'markdown', 'md', 'pdf', 'html', 'htm', 'xlsx', 'docx', 'csv',
'jpg', 'jpeg', 'png', 'webp', 'gif'] 'jpg', 'jpeg', 'png', 'webp', 'gif', 'svg']
IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'] IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg']
PREVIEW_WORDS_LIMIT = 3000 PREVIEW_WORDS_LIMIT = 3000
...@@ -154,3 +154,21 @@ class FileService: ...@@ -154,3 +154,21 @@ class FileService:
generator = storage.load(upload_file.key, stream=True) generator = storage.load(upload_file.key, stream=True)
return generator, upload_file.mime_type return generator, upload_file.mime_type
@staticmethod
def get_public_image_preview(file_id: str) -> str:
upload_file = db.session.query(UploadFile) \
.filter(UploadFile.id == file_id) \
.first()
if not upload_file:
raise NotFound("File not found or signature is invalid")
# extract text from file
extension = upload_file.extension
if extension.lower() not in IMAGE_EXTENSIONS:
raise UnsupportedFileTypeError()
generator = storage.load(upload_file.key)
return generator, upload_file.mime_type
from flask_login import current_user from flask_login import current_user
from extensions.ext_database import db from extensions.ext_database import db
from models.account import Tenant, TenantAccountJoin from models.account import Tenant, TenantAccountJoin, TenantAccountJoinRole
from models.provider import Provider from models.provider import Provider
from services.billing_service import BillingService
from services.account_service import TenantService
class WorkspaceService: class WorkspaceService:
@classmethod @classmethod
...@@ -28,6 +31,11 @@ class WorkspaceService: ...@@ -28,6 +31,11 @@ class WorkspaceService:
).first() ).first()
tenant_info['role'] = tenant_account_join.role tenant_info['role'] = tenant_account_join.role
billing_info = BillingService.get_info(tenant_info['id'])
if billing_info['can_replace_logo'] and TenantService.has_roles(tenant, [TenantAccountJoinRole.OWNER, TenantAccountJoinRole.ADMIN]):
tenant_info['custom_config'] = tenant.custom_config_dict
# Get providers # Get providers
providers = db.session.query(Provider).filter( providers = db.session.query(Provider).filter(
Provider.tenant_id == tenant.id Provider.tenant_id == tenant.id
......
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="colors">
<path id="Icon" d="M12 20.4722C13.0615 21.4223 14.4633 22 16 22C19.3137 22 22 19.3137 22 16C22 13.2331 20.1271 10.9036 17.5798 10.2102M6.42018 10.2102C3.87293 10.9036 2 13.2331 2 16C2 19.3137 4.68629 22 8 22C11.3137 22 14 19.3137 14 16C14 15.2195 13.851 14.4738 13.5798 13.7898M18 8C18 11.3137 15.3137 14 12 14C8.68629 14 6 11.3137 6 8C6 4.68629 8.68629 2 12 2C15.3137 2 18 4.68629 18 8Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="message-dots-circle">
<path id="Solid" fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.47715 2 2 6.47715 2 12C2 13.3283 2.25952 14.5985 2.73156 15.7608C2.77419 15.8658 2.79872 15.9264 2.81552 15.9711L2.82063 15.9849L2.82 15.9897C2.815 16.0266 2.80672 16.0769 2.79071 16.173L2.19294 19.7596C2.16612 19.9202 2.13611 20.0999 2.12433 20.256C2.11148 20.4261 2.10701 20.6969 2.22973 20.983C2.38144 21.3367 2.6633 21.6186 3.017 21.7703C3.30312 21.893 3.57386 21.8885 3.74404 21.8757C3.90013 21.8639 4.07985 21.8339 4.24049 21.8071L7.82705 21.2093C7.92309 21.1933 7.97339 21.185 8.0103 21.18L8.01505 21.1794L8.02887 21.1845C8.07362 21.2013 8.13423 21.2258 8.23921 21.2684C9.4015 21.7405 10.6717 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM6 12C6 11.1716 6.67157 10.5 7.5 10.5C8.32843 10.5 9 11.1716 9 12C9 12.8284 8.32843 13.5 7.5 13.5C6.67157 13.5 6 12.8284 6 12ZM10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5C11.1716 13.5 10.5 12.8284 10.5 12ZM16.5 10.5C15.6716 10.5 15 11.1716 15 12C15 12.8284 15.6716 13.5 16.5 13.5C17.3284 13.5 18 12.8284 18 12C18 11.1716 17.3284 10.5 16.5 10.5Z" fill="black"/>
</g>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="colors">
<g id="Solid">
<path d="M13.4494 13.2298C12.9854 13.3409 12.5002 13.3999 12 13.3999C10.2804 13.3999 8.72326 12.6997 7.59953 11.5677C6.4872 10.4471 5.8 8.90382 5.8 7.20007C5.8 3.77586 8.57584 1 12 1C15.4241 1 18.2 3.77586 18.2 7.20007C18.2 8.44569 17.8327 9.60551 17.2005 10.5771C16.3665 11.8588 15.0715 12.8131 13.5506 13.2047C13.517 13.2133 13.4833 13.2217 13.4494 13.2298Z" fill="black"/>
<path d="M15.1476 14.7743C16.6646 14.1431 17.9513 13.0695 18.8465 11.7146C19.0004 11.4817 19.0773 11.3652 19.1762 11.3066C19.2615 11.2561 19.3659 11.2312 19.4648 11.2379C19.5795 11.2457 19.6773 11.3015 19.8728 11.4133C21.7413 12.4817 23 14.4946 23 16.7999C23 20.2241 20.2242 23 16.8 23C15.9123 23 15.0689 22.8139 14.3059 22.4782C14.0549 22.3678 13.9294 22.3126 13.8502 22.2049C13.7822 22.1126 13.7468 21.9922 13.7539 21.8777C13.7622 21.7444 13.8565 21.6018 14.045 21.3167C14.8373 20.1184 15.3234 18.6997 15.3917 17.1723C15.3969 17.0566 15.3996 16.9402 15.4 16.8233L15.4 16.7999C15.4 16.1888 15.333 15.5926 15.2057 15.0185C15.1876 14.9366 15.1682 14.8552 15.1476 14.7743Z" fill="black"/>
<path d="M4.12723 11.4133C4.32273 11.3015 4.42049 11.2457 4.53516 11.2379C4.63414 11.2312 4.73848 11.2561 4.82382 11.3066C4.92269 11.3652 4.99964 11.4817 5.15355 11.7146C6.62074 13.9352 9.13929 15.4001 12 15.4001C12.4146 15.4001 12.822 15.3694 13.2201 15.31L13.2263 15.3357C13.3398 15.8045 13.4 16.2947 13.4 16.7999L13.4 16.8214C13.3997 16.9056 13.3977 16.9895 13.3941 17.0728C13.2513 20.3704 10.5327 23 7.2 23C3.77584 23 1 20.2241 1 16.7999C1 14.4946 2.25869 12.4817 4.12723 11.4133Z" fill="black"/>
</g>
</g>
</svg>
{
"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": "g",
"attributes": {
"id": "colors"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M12 20.4722C13.0615 21.4223 14.4633 22 16 22C19.3137 22 22 19.3137 22 16C22 13.2331 20.1271 10.9036 17.5798 10.2102M6.42018 10.2102C3.87293 10.9036 2 13.2331 2 16C2 19.3137 4.68629 22 8 22C11.3137 22 14 19.3137 14 16C14 15.2195 13.851 14.4738 13.5798 13.7898M18 8C18 11.3137 15.3137 14 12 14C8.68629 14 6 11.3137 6 8C6 4.68629 8.68629 2 12 2C15.3137 2 18 4.68629 18 8Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "Colors"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Colors.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Colors'
export default Icon
export { default as BezierCurve03 } from './BezierCurve03' export { default as BezierCurve03 } from './BezierCurve03'
export { default as Colors } from './Colors'
export { default as TypeSquare } from './TypeSquare' export { default as TypeSquare } from './TypeSquare'
{
"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": "g",
"attributes": {
"id": "message-dots-circle"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Solid",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M12 2C6.47715 2 2 6.47715 2 12C2 13.3283 2.25952 14.5985 2.73156 15.7608C2.77419 15.8658 2.79872 15.9264 2.81552 15.9711L2.82063 15.9849L2.82 15.9897C2.815 16.0266 2.80672 16.0769 2.79071 16.173L2.19294 19.7596C2.16612 19.9202 2.13611 20.0999 2.12433 20.256C2.11148 20.4261 2.10701 20.6969 2.22973 20.983C2.38144 21.3367 2.6633 21.6186 3.017 21.7703C3.30312 21.893 3.57386 21.8885 3.74404 21.8757C3.90013 21.8639 4.07985 21.8339 4.24049 21.8071L7.82705 21.2093C7.92309 21.1933 7.97339 21.185 8.0103 21.18L8.01505 21.1794L8.02887 21.1845C8.07362 21.2013 8.13423 21.2258 8.23921 21.2684C9.4015 21.7405 10.6717 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM6 12C6 11.1716 6.67157 10.5 7.5 10.5C8.32843 10.5 9 11.1716 9 12C9 12.8284 8.32843 13.5 7.5 13.5C6.67157 13.5 6 12.8284 6 12ZM10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5C11.1716 13.5 10.5 12.8284 10.5 12ZM16.5 10.5C15.6716 10.5 15 11.1716 15 12C15 12.8284 15.6716 13.5 16.5 13.5C17.3284 13.5 18 12.8284 18 12C18 11.1716 17.3284 10.5 16.5 10.5Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "MessageDotsCircle"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './MessageDotsCircle.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'MessageDotsCircle'
export default Icon
export { default as MessageDotsCircle } from './MessageDotsCircle'
export { default as MessageFast } from './MessageFast' export { default as MessageFast } from './MessageFast'
{
"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": "g",
"attributes": {
"id": "colors"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Solid"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M13.4494 13.2298C12.9854 13.3409 12.5002 13.3999 12 13.3999C10.2804 13.3999 8.72326 12.6997 7.59953 11.5677C6.4872 10.4471 5.8 8.90382 5.8 7.20007C5.8 3.77586 8.57584 1 12 1C15.4241 1 18.2 3.77586 18.2 7.20007C18.2 8.44569 17.8327 9.60551 17.2005 10.5771C16.3665 11.8588 15.0715 12.8131 13.5506 13.2047C13.517 13.2133 13.4833 13.2217 13.4494 13.2298Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M15.1476 14.7743C16.6646 14.1431 17.9513 13.0695 18.8465 11.7146C19.0004 11.4817 19.0773 11.3652 19.1762 11.3066C19.2615 11.2561 19.3659 11.2312 19.4648 11.2379C19.5795 11.2457 19.6773 11.3015 19.8728 11.4133C21.7413 12.4817 23 14.4946 23 16.7999C23 20.2241 20.2242 23 16.8 23C15.9123 23 15.0689 22.8139 14.3059 22.4782C14.0549 22.3678 13.9294 22.3126 13.8502 22.2049C13.7822 22.1126 13.7468 21.9922 13.7539 21.8777C13.7622 21.7444 13.8565 21.6018 14.045 21.3167C14.8373 20.1184 15.3234 18.6997 15.3917 17.1723C15.3969 17.0566 15.3996 16.9402 15.4 16.8233L15.4 16.7999C15.4 16.1888 15.333 15.5926 15.2057 15.0185C15.1876 14.9366 15.1682 14.8552 15.1476 14.7743Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.12723 11.4133C4.32273 11.3015 4.42049 11.2457 4.53516 11.2379C4.63414 11.2312 4.73848 11.2561 4.82382 11.3066C4.92269 11.3652 4.99964 11.4817 5.15355 11.7146C6.62074 13.9352 9.13929 15.4001 12 15.4001C12.4146 15.4001 12.822 15.3694 13.2201 15.31L13.2263 15.3357C13.3398 15.8045 13.4 16.2947 13.4 16.7999L13.4 16.8214C13.3997 16.9056 13.3977 16.9895 13.3941 17.0728C13.2513 20.3704 10.5327 23 7.2 23C3.77584 23 1 20.2241 1 16.7999C1 14.4946 2.25869 12.4817 4.12723 11.4133Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "Colors"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Colors.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Colors'
export default Icon
export { default as Brush01 } from './Brush01' export { default as Brush01 } from './Brush01'
export { default as Citations } from './Citations' export { default as Citations } from './Citations'
export { default as Colors } from './Colors'
export { default as Paragraph } from './Paragraph' export { default as Paragraph } from './Paragraph'
export { default as TypeSquare } from './TypeSquare' export { default as TypeSquare } from './TypeSquare'
...@@ -6,13 +6,13 @@ type ImageUploadParams = { ...@@ -6,13 +6,13 @@ type ImageUploadParams = {
onSuccessCallback: (res: { id: string }) => void onSuccessCallback: (res: { id: string }) => void
onErrorCallback: () => void onErrorCallback: () => void
} }
type ImageUpload = (v: ImageUploadParams, isPublic?: boolean) => void type ImageUpload = (v: ImageUploadParams, isPublic?: boolean, url?: string) => void
export const imageUpload: ImageUpload = ({ export const imageUpload: ImageUpload = ({
file, file,
onProgressCallback, onProgressCallback,
onSuccessCallback, onSuccessCallback,
onErrorCallback, onErrorCallback,
}, isPublic) => { }, isPublic, url) => {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
const onProgress = (e: ProgressEvent) => { const onProgress = (e: ProgressEvent) => {
...@@ -26,7 +26,7 @@ export const imageUpload: ImageUpload = ({ ...@@ -26,7 +26,7 @@ export const imageUpload: ImageUpload = ({
xhr: new XMLHttpRequest(), xhr: new XMLHttpRequest(),
data: formData, data: formData,
onprogress: onProgress, onprogress: onProgress,
}, isPublic) }, isPublic, url)
.then((res: { id: string }) => { .then((res: { id: string }) => {
onSuccessCallback(res) onSuccessCallback(res)
}) })
......
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import Button from '@/app/components/base/button'
import { Grid01 } from '@/app/components/base/icons/src/vender/solid/layout'
import { Container, Database01 } from '@/app/components/base/icons/src/vender/line/development'
import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
const CustomAppHeaderBrand = () => {
const { t } = useTranslation()
const { plan } = useProviderContext()
return (
<div className='py-3'>
<div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.app.title')}</div>
<div className='relative mb-4 rounded-xl bg-gray-100 border-[0.5px] border-black/[0.08] shadow-xs'>
<div className={`${s.mask} absolute inset-0 rounded-xl`}></div>
<div className='flex items-center pl-5 h-14 rounded-t-xl'>
<div className='relative flex items-center mr-[199px] w-[120px] h-10 bg-[rgba(217,45,32,0.12)]'>
<div className='ml-[1px] mr-[3px] w-[34px] h-[34px] border-8 border-black/[0.16] rounded-full'></div>
<div className='text-[13px] font-bold text-black/[0.24]'>YOUR LOGO</div>
<div className='absolute top-0 bottom-0 left-0.5 w-[0.5px] bg-[#F97066] opacity-50'></div>
<div className='absolute top-0 bottom-0 right-0.5 w-[0.5px] bg-[#F97066] opacity-50'></div>
<div className='absolute left-0 right-0 top-0.5 h-[0.5px] bg-[#F97066] opacity-50'></div>
<div className='absolute left-0 right-0 bottom-0.5 h-[0.5px] bg-[#F97066] opacity-50'></div>
</div>
<div className='flex items-center mr-3 px-3 h-7 rounded-xl bg-white shadow-xs'>
<Grid01 className='shrink-0 mr-2 w-4 h-4 text-[#155eef]' />
<div className='w-12 h-1.5 rounded-[5px] bg-[#155eef] opacity-80'></div>
</div>
<div className='flex items-center mr-3 px-3 h-7'>
<Container className='shrink-0 mr-2 w-4 h-4 text-gray-500' />
<div className='w-[50px] h-1.5 rounded-[5px] bg-gray-300'></div>
</div>
<div className='flex items-center px-3 h-7'>
<Database01 className='shrink-0 mr-2 w-4 h-4 text-gray-500' />
<div className='w-14 h-1.5 rounded-[5px] bg-gray-300 opacity-80'></div>
</div>
</div>
<div className='h-8 border-t border-t-gray-200 rounded-b-xl'></div>
</div>
<div className='flex items-center mb-2'>
<Button
className={`
!h-8 !px-3 bg-white !text-[13px]
${plan.type === Plan.sandbox ? 'opacity-40' : ''}
`}
disabled={plan.type === Plan.sandbox}
>
<ImagePlus className='mr-2 w-4 h-4' />
{t('custom.upload')}
</Button>
<div className='mx-2 h-5 w-[1px] bg-black/5'></div>
<Button
className={`
!h-8 !px-3 bg-white !text-[13px]
${plan.type === Plan.sandbox ? 'opacity-40' : ''}
`}
disabled={plan.type === Plan.sandbox}
>
{t('custom.restore')}
</Button>
</div>
<div className='text-xs text-gray-500'>{t('custom.app.changeLogoTip')}</div>
</div>
)
}
export default CustomAppHeaderBrand
.mask {
background: linear-gradient(95deg, rgba(255, 255, 255, 0.00) 43.9%, rgba(255, 255, 255, 0.80) 95.76%); ;
}
\ No newline at end of file
import { useTranslation } from 'react-i18next'
import CustomWebAppBrand from '../custom-web-app-brand'
import CustomAppHeaderBrand from '../custom-app-header-brand'
import s from '../style.module.css'
import GridMask from '@/app/components/base/grid-mask'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
import { contactSalesUrl } from '@/app/components/billing/config'
const CustomPage = () => {
const { t } = useTranslation()
const { plan } = useProviderContext()
return (
<div className='flex flex-col'>
{
plan.type === Plan.sandbox && (
<GridMask canvasClassName='!rounded-xl'>
<div className='flex justify-between mb-1 px-6 py-5 h-[88px] shadow-md rounded-xl border-[0.5px] border-gray-200'>
<div className={`${s.textGradient} leading-[24px] text-base font-semibold`}>
<div>{t('custom.upgradeTip.prefix')}</div>
<div>{t('custom.upgradeTip.suffix')}</div>
</div>
<UpgradeBtn />
</div>
</GridMask>
)
}
<CustomWebAppBrand />
{
plan.type === Plan.sandbox && (
<>
<div className='my-2 h-[0.5px] bg-gray-100'></div>
<CustomAppHeaderBrand />
</>
)
}
{
(plan.type === Plan.professional || plan.type === Plan.team) && (
<div className='absolute bottom-0 h-[50px] leading-[50px] text-xs text-gray-500'>
{t('custom.customize.prefix')}
<a className='text-[#155EEF]' href={contactSalesUrl} target='_blank'>{t('custom.customize.contactUs')}</a>
{t('custom.customize.suffix')}
</div>
)
}
</div>
)
}
export default CustomPage
import type { ChangeEvent } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import LogoSite from '@/app/components/base/logo/logo-site'
import Switch from '@/app/components/base/switch'
import Button from '@/app/components/base/button'
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
import { imageUpload } from '@/app/components/base/image-uploader/utils'
import type {} from '@/app/components/base/image-uploader/utils'
import { useToastContext } from '@/app/components/base/toast'
import {
updateCurrentWorkspace,
} from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { API_PREFIX } from '@/config'
const ALLOW_FILE_EXTENSIONS = ['svg', 'png']
const CustomWebAppBrand = () => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { plan } = useProviderContext()
const {
currentWorkspace,
mutateCurrentWorkspace,
isCurrentWorkspaceManager,
} = useAppContext()
const [fileId, setFileId] = useState('')
const [uploadProgress, setUploadProgress] = useState(0)
const isSandbox = plan.type === Plan.sandbox
const uploading = uploadProgress > 0 && uploadProgress < 100
const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || ''
const webappBrandRemoved = currentWorkspace.custom_config?.remove_webapp_brand
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file)
return
if (file.size > 5 * 1024 * 1024) {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: 5 }) })
return
}
imageUpload({
file,
onProgressCallback: (progress) => {
setUploadProgress(progress)
},
onSuccessCallback: (res) => {
setUploadProgress(100)
setFileId(res.id)
},
onErrorCallback: () => {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
setUploadProgress(-1)
},
}, false, '/workspaces/custom-config/webapp-logo/upload')
}
const handleApply = async () => {
await updateCurrentWorkspace({
url: '/workspaces/custom-config',
body: {
remove_webapp_brand: webappBrandRemoved,
replace_webapp_logo: fileId,
},
})
mutateCurrentWorkspace()
setFileId('')
}
const handleRestore = async () => {
await updateCurrentWorkspace({
url: '/workspaces/custom-config',
body: {
remove_webapp_brand: false,
replace_webapp_logo: null,
},
})
mutateCurrentWorkspace()
}
const handleSwitch = async (checked: boolean) => {
await updateCurrentWorkspace({
url: '/workspaces/custom-config',
body: {
remove_webapp_brand: checked,
replace_webapp_logo: webappLogo,
},
})
mutateCurrentWorkspace()
}
const handleCancel = () => {
setFileId('')
setUploadProgress(0)
}
return (
<div className='py-4'>
<div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.webapp.title')}</div>
<div className='relative mb-4 pl-4 pb-6 pr-[119px] rounded-xl border-[0.5px] border-black/[0.08] shadow-xs bg-gray-50 overflow-hidden'>
<div className={`${s.mask} absolute top-0 left-0 w-full -bottom-2 z-10`}></div>
<div className='flex items-center -mt-2 mb-4 p-6 bg-white rounded-xl'>
<div className='flex items-center px-4 w-[125px] h-9 rounded-lg bg-primary-600 border-[0.5px] border-primary-700 shadow-xs'>
<MessageDotsCircle className='shrink-0 mr-2 w-4 h-4 text-white' />
<div className='grow h-2 rounded-sm bg-white opacity-50' />
</div>
</div>
<div className='flex items-center h-5 justify-between'>
<div className='w-[369px] h-1.5 rounded-sm bg-gray-200 opacity-80' />
{
!webappBrandRemoved && (
<div className='flex items-center text-[10px] font-medium text-gray-400'>
POWERED BY
{
webappLogo
? <img key={webappLogo} src={`${API_PREFIX.slice(0, -12)}/files/workspaces/${currentWorkspace.id}/webapp-logo`} alt='logo' className='ml-2 block w-auto h-5' />
: <LogoSite className='ml-2 !h-5' />
}
</div>
)
}
</div>
</div>
<div className='flex items-center justify-between mb-2 px-4 h-14 rounded-xl border-[0.5px] border-gray-200 bg-gray-50 text-sm font-medium text-gray-900'>
{t('custom.webapp.removeBrand')}
<Switch
size='l'
defaultValue={webappBrandRemoved}
disabled={isSandbox || !isCurrentWorkspaceManager}
onChange={handleSwitch}
/>
</div>
<div className={`
flex items-center justify-between px-4 py-3 rounded-xl border-[0.5px] border-gray-200 bg-gray-50
${webappBrandRemoved && 'opacity-30'}
`}>
<div>
<div className='leading-5 text-sm font-medium text-gray-900'>{t('custom.webapp.changeLogo')}</div>
<div className='leading-[18px] text-xs text-gray-500'>{t('custom.webapp.changeLogoTip')}</div>
</div>
<div className='flex items-center'>
{
!uploading && (
<Button
className={`
relative mr-2 !h-8 !px-3 bg-white !text-[13px]
${isSandbox ? 'opacity-40' : ''}
`}
disabled={isSandbox || webappBrandRemoved || !isCurrentWorkspaceManager}
>
<ImagePlus className='mr-2 w-4 h-4' />
{
(webappLogo || fileId)
? t('custom.change')
: t('custom.upload')
}
<input
className={`
absolute block inset-0 opacity-0 text-[0] w-full
${(isSandbox || webappBrandRemoved) ? 'cursor-not-allowed' : 'cursor-pointer'}
`}
onClick={e => (e.target as HTMLInputElement).value = ''}
type='file'
accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')}
onChange={handleChange}
disabled={isSandbox || webappBrandRemoved || !isCurrentWorkspaceManager}
/>
</Button>
)
}
{
uploading && (
<Button
className='relative mr-2 !h-8 !px-3 bg-white !text-[13px] opacity-40'
disabled={true}
>
<Loading02 className='animate-spin mr-2 w-4 h-4' />
{t('custom.uploading')}
</Button>
)
}
{
fileId && (
<>
<Button
type='primary'
className='mr-2 !h-8 !px-3 !py-0 !text-[13px]'
onClick={handleApply}
disabled={webappBrandRemoved || !isCurrentWorkspaceManager}
>
{t('custom.apply')}
</Button>
<Button
className='mr-2 !h-8 !px-3 !text-[13px] bg-white'
onClick={handleCancel}
disabled={webappBrandRemoved || !isCurrentWorkspaceManager}
>
{t('common.operation.cancel')}
</Button>
</>
)
}
<div className='mr-2 h-5 w-[1px] bg-black/5'></div>
<Button
className={`
!h-8 !px-3 bg-white !text-[13px]
${isSandbox ? 'opacity-40' : ''}
`}
disabled={isSandbox || (!webappLogo && !webappBrandRemoved) || webappBrandRemoved || !isCurrentWorkspaceManager}
onClick={handleRestore}
>
{t('custom.restore')}
</Button>
</div>
</div>
{
uploadProgress === -1 && (
<div className='mt-2 text-xs text-[#D92D20]'>{t('custom.uploadedFail')}</div>
)
}
</div>
)
}
export default CustomWebAppBrand
.mask {
background: linear-gradient(273deg, rgba(255, 255, 255, 0.00) 51.75%, rgba(255, 255, 255, 0.80) 115.32%);
}
\ No newline at end of file
.textGradient {
background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
\ No newline at end of file
...@@ -14,6 +14,7 @@ import DataSourcePage from './data-source-page' ...@@ -14,6 +14,7 @@ import DataSourcePage from './data-source-page'
import ModelPage from './model-page' import ModelPage from './model-page'
import s from './index.module.css' import s from './index.module.css'
import BillingPage from '@/app/components/billing/billing-page' import BillingPage from '@/app/components/billing/billing-page'
import CustomPage from '@/app/components/custom/custom-page'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import { import {
Database03, Database03,
...@@ -26,8 +27,11 @@ import { User01 as User01Solid, Users01 as Users01Solid } from '@/app/components ...@@ -26,8 +27,11 @@ import { User01 as User01Solid, Users01 as Users01Solid } from '@/app/components
import { Globe01 } from '@/app/components/base/icons/src/vender/line/mapsAndTravel' import { Globe01 } from '@/app/components/base/icons/src/vender/line/mapsAndTravel'
import { AtSign, XClose } from '@/app/components/base/icons/src/vender/line/general' import { AtSign, XClose } from '@/app/components/base/icons/src/vender/line/general'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes' import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import { Colors } from '@/app/components/base/icons/src/vender/line/editor'
import { Colors as ColorsSolid } from '@/app/components/base/icons/src/vender/solid/editor'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { IS_CE_EDITION } from '@/config'
const iconClassName = ` const iconClassName = `
w-4 h-4 ml-3 mr-2 w-4 h-4 ml-3 mr-2
...@@ -96,6 +100,12 @@ export default function AccountSetting({ ...@@ -96,6 +100,12 @@ export default function AccountSetting({
icon: <Webhooks className={iconClassName} />, icon: <Webhooks className={iconClassName} />,
activeIcon: <Webhooks className={iconClassName} />, activeIcon: <Webhooks className={iconClassName} />,
}, },
{
key: IS_CE_EDITION ? false : 'custom',
name: t('custom.custom'),
icon: <Colors className={iconClassName} />,
activeIcon: <ColorsSolid className={iconClassName} />,
},
].filter(item => !!item.key) as GroupItem[] ].filter(item => !!item.key) as GroupItem[]
})() })()
...@@ -206,6 +216,7 @@ export default function AccountSetting({ ...@@ -206,6 +216,7 @@ export default function AccountSetting({
{activeMenu === 'data-source' && <DataSourcePage />} {activeMenu === 'data-source' && <DataSourcePage />}
{activeMenu === 'plugin' && <PluginPage />} {activeMenu === 'plugin' && <PluginPage />}
{activeMenu === 'api-based-extension' && <ApiBasedExtensionPage /> } {activeMenu === 'api-based-extension' && <ApiBasedExtensionPage /> }
{activeMenu === 'custom' && <CustomPage /> }
</div> </div>
</div> </div>
</div> </div>
......
...@@ -71,6 +71,7 @@ const Main: FC<IMainProps> = ({ ...@@ -71,6 +71,7 @@ const Main: FC<IMainProps> = ({
const [inited, setInited] = useState<boolean>(false) const [inited, setInited] = useState<boolean>(false)
const [plan, setPlan] = useState<string>('basic') // basic/plus/pro const [plan, setPlan] = useState<string>('basic') // basic/plus/pro
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false) const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
const [customConfig, setCustomConfig] = useState<any>(null)
// in mobile, show sidebar by click button // in mobile, show sidebar by click button
const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false) const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false)
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client.
...@@ -364,10 +365,11 @@ const Main: FC<IMainProps> = ({ ...@@ -364,10 +365,11 @@ const Main: FC<IMainProps> = ({
(async () => { (async () => {
try { try {
const [appData, conversationData, appParams]: any = await fetchInitData() const [appData, conversationData, appParams]: any = await fetchInitData()
const { app_id: appId, site: siteInfo, plan, can_replace_logo }: any = appData const { app_id: appId, site: siteInfo, plan, can_replace_logo, custom_config }: any = appData
setAppId(appId) setAppId(appId)
setPlan(plan) setPlan(plan)
setCanReplaceLogo(can_replace_logo) setCanReplaceLogo(can_replace_logo)
setCustomConfig(custom_config)
const tempIsPublicVersion = siteInfo.prompt_public const tempIsPublicVersion = siteInfo.prompt_public
setIsPublicVersion(tempIsPublicVersion) setIsPublicVersion(tempIsPublicVersion)
const prompt_template = '' const prompt_template = ''
...@@ -752,6 +754,7 @@ const Main: FC<IMainProps> = ({ ...@@ -752,6 +754,7 @@ const Main: FC<IMainProps> = ({
onInputsChange={setCurrInputs} onInputsChange={setCurrInputs}
plan={plan} plan={plan}
canReplaceLogo={canReplaceLogo} canReplaceLogo={canReplaceLogo}
customConfig={customConfig}
></ConfigSence> ></ConfigSence>
{ {
......
...@@ -27,6 +27,10 @@ export type IWelcomeProps = { ...@@ -27,6 +27,10 @@ export type IWelcomeProps = {
onInputsChange: (inputs: Record<string, any>) => void onInputsChange: (inputs: Record<string, any>) => void
plan?: string plan?: string
canReplaceLogo?: boolean canReplaceLogo?: boolean
customConfig?: {
remove_webapp_brand?: boolean
replace_webapp_logo?: string
}
} }
const Welcome: FC<IWelcomeProps> = ({ const Welcome: FC<IWelcomeProps> = ({
...@@ -34,13 +38,12 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -34,13 +38,12 @@ const Welcome: FC<IWelcomeProps> = ({
hasSetInputs, hasSetInputs,
isPublicVersion, isPublicVersion,
siteInfo, siteInfo,
plan,
promptConfig, promptConfig,
onStartChat, onStartChat,
canEidtInpus, canEidtInpus,
savedInputs, savedInputs,
onInputsChange, onInputsChange,
canReplaceLogo, customConfig,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const hasVar = promptConfig.prompt_variables.length > 0 const hasVar = promptConfig.prompt_variables.length > 0
...@@ -352,10 +355,20 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -352,10 +355,20 @@ const Welcome: FC<IWelcomeProps> = ({
</div> </div>
: <div> : <div>
</div>} </div>}
{!canReplaceLogo && <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank"> {
<span className='uppercase'>{t('share.chat.powerBy')}</span> customConfig?.remove_webapp_brand
<FootLogo /> ? null
</a>} : (
<a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
<span className='uppercase'>{t('share.chat.powerBy')}</span>
{
customConfig?.replace_webapp_logo
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
: <FootLogo />
}
</a>
)
}
</div> </div>
)} )}
</div> </div>
......
...@@ -55,6 +55,7 @@ const Main: FC<IMainProps> = ({ ...@@ -55,6 +55,7 @@ const Main: FC<IMainProps> = ({
const [inited, setInited] = useState<boolean>(false) const [inited, setInited] = useState<boolean>(false)
const [plan, setPlan] = useState<string>('basic') // basic/plus/pro const [plan, setPlan] = useState<string>('basic') // basic/plus/pro
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false) const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
const [customConfig, setCustomConfig] = useState<any>(null)
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client.
useEffect(() => { useEffect(() => {
if (siteInfo?.title) { if (siteInfo?.title) {
...@@ -283,10 +284,11 @@ const Main: FC<IMainProps> = ({ ...@@ -283,10 +284,11 @@ const Main: FC<IMainProps> = ({
(async () => { (async () => {
try { try {
const [appData, conversationData, appParams]: any = await fetchInitData() const [appData, conversationData, appParams]: any = await fetchInitData()
const { app_id: appId, site: siteInfo, plan, can_replace_logo }: any = appData const { app_id: appId, site: siteInfo, plan, can_replace_logo, custom_config }: any = appData
setAppId(appId) setAppId(appId)
setPlan(plan) setPlan(plan)
setCanReplaceLogo(can_replace_logo) setCanReplaceLogo(can_replace_logo)
setCustomConfig(custom_config)
const tempIsPublicVersion = siteInfo.prompt_public const tempIsPublicVersion = siteInfo.prompt_public
setIsPublicVersion(tempIsPublicVersion) setIsPublicVersion(tempIsPublicVersion)
const prompt_template = '' const prompt_template = ''
...@@ -592,6 +594,7 @@ const Main: FC<IMainProps> = ({ ...@@ -592,6 +594,7 @@ const Main: FC<IMainProps> = ({
onInputsChange={setCurrInputs} onInputsChange={setCurrInputs}
plan={plan} plan={plan}
canReplaceLogo={canReplaceLogo} canReplaceLogo={canReplaceLogo}
customConfig={customConfig}
></ConfigScene> ></ConfigScene>
{ {
shouldReload && ( shouldReload && (
......
...@@ -27,6 +27,10 @@ export type IWelcomeProps = { ...@@ -27,6 +27,10 @@ export type IWelcomeProps = {
onInputsChange: (inputs: Record<string, any>) => void onInputsChange: (inputs: Record<string, any>) => void
plan: string plan: string
canReplaceLogo?: boolean canReplaceLogo?: boolean
customConfig?: {
remove_webapp_brand?: boolean
replace_webapp_logo?: string
}
} }
const Welcome: FC<IWelcomeProps> = ({ const Welcome: FC<IWelcomeProps> = ({
...@@ -34,13 +38,12 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -34,13 +38,12 @@ const Welcome: FC<IWelcomeProps> = ({
hasSetInputs, hasSetInputs,
isPublicVersion, isPublicVersion,
siteInfo, siteInfo,
plan,
promptConfig, promptConfig,
onStartChat, onStartChat,
canEditInputs, canEditInputs,
savedInputs, savedInputs,
onInputsChange, onInputsChange,
canReplaceLogo, customConfig,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const hasVar = promptConfig.prompt_variables.length > 0 const hasVar = promptConfig.prompt_variables.length > 0
...@@ -353,10 +356,20 @@ const Welcome: FC<IWelcomeProps> = ({ ...@@ -353,10 +356,20 @@ const Welcome: FC<IWelcomeProps> = ({
</div> </div>
: <div> : <div>
</div>} </div>}
{!canReplaceLogo && <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank"> {
<span className='uppercase'>{t('share.chat.powerBy')}</span> customConfig?.remove_webapp_brand
<FootLogo /> ? null
</a>} : (
<a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
<span className='uppercase'>{t('share.chat.powerBy')}</span>
{
customConfig?.replace_webapp_logo
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
: <FootLogo />
}
</a>
)
}
</div> </div>
)} )}
</div> </div>
......
...@@ -37,6 +37,8 @@ import exploreEn from './lang/explore.en' ...@@ -37,6 +37,8 @@ import exploreEn from './lang/explore.en'
import exploreZh from './lang/explore.zh' import exploreZh from './lang/explore.zh'
import billingEn from './lang/billing.en' import billingEn from './lang/billing.en'
import billingZh from './lang/billing.zh' import billingZh from './lang/billing.zh'
import customEn from './lang/custom.en'
import customZh from './lang/custom.zh'
const resources = { const resources = {
'en': { 'en': {
...@@ -62,6 +64,7 @@ const resources = { ...@@ -62,6 +64,7 @@ const resources = {
explore: exploreEn, explore: exploreEn,
// billing // billing
billing: billingEn, billing: billingEn,
custom: customEn,
}, },
}, },
'zh-Hans': { 'zh-Hans': {
...@@ -86,6 +89,7 @@ const resources = { ...@@ -86,6 +89,7 @@ const resources = {
datasetCreation: datasetCreationZh, datasetCreation: datasetCreationZh,
explore: exploreZh, explore: exploreZh,
billing: billingZh, billing: billingZh,
custom: customZh,
}, },
}, },
} }
......
const translation = {
custom: 'Customization',
upgradeTip: {
prefix: 'Upgrade your plan to',
suffix: 'customize your brand.',
},
webapp: {
title: 'Customize web app brand',
removeBrand: 'Remove Powered by Dify',
changeLogo: 'Change Powered by Brand Image',
changeLogoTip: 'SVG or PNG format with a minimum size of 40x40px',
},
app: {
title: 'Customize app header brand',
changeLogoTip: 'SVG or PNG format with a minimum size of 80x80px',
},
upload: 'Upload',
uploading: 'Uploading',
uploadedFail: 'Image upload failed, please re-upload.',
change: 'Change',
apply: 'Apply',
restore: 'Restore Defaults',
customize: {
contactUs: ' contact us ',
prefix: 'To customize the brand logo within the app, please',
suffix: 'to upgrade to the Enterprise edition.',
},
}
export default translation
const translation = {
custom: '定制',
upgradeTip: {
prefix: '升级您的计划以',
suffix: '定制您的品牌。',
},
webapp: {
title: '定制 web app 品牌',
removeBrand: '移除 Powered by Dify',
changeLogo: '更改 Powered by Brand 图片',
changeLogoTip: 'SVG 或 PNG 格式,最小尺寸为 40x40px',
},
app: {
title: '定制应用品牌',
changeLogoTip: 'SVG 或 PNG 格式,最小尺寸为 80x80px',
},
upload: '上传',
uploading: '上传中',
uploadedFail: '图片上传失败,请重新上传。',
change: '更改',
apply: '应用',
restore: '恢复默认',
customize: {
contactUs: '联系我们',
prefix: '如需在 Dify 内自定义品牌图标,请',
suffix: '升级至企业版。',
},
}
export default translation
...@@ -123,6 +123,10 @@ export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & { ...@@ -123,6 +123,10 @@ export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & {
providers: Provider[] providers: Provider[]
in_trail: boolean in_trail: boolean
trial_end_reason?: string trial_end_reason?: string
custom_config?: {
remove_webapp_brand?: boolean
replace_webapp_logo?: string
}
} }
export type DataSourceNotionPage = { export type DataSourceNotionPage = {
......
...@@ -297,7 +297,7 @@ const baseFetch = <T>( ...@@ -297,7 +297,7 @@ const baseFetch = <T>(
]) as Promise<T> ]) as Promise<T>
} }
export const upload = (options: any, isPublicAPI?: boolean): Promise<any> => { export const upload = (options: any, isPublicAPI?: boolean, url?: string): Promise<any> => {
const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
let token = '' let token = ''
if (isPublicAPI) { if (isPublicAPI) {
...@@ -318,7 +318,7 @@ export const upload = (options: any, isPublicAPI?: boolean): Promise<any> => { ...@@ -318,7 +318,7 @@ export const upload = (options: any, isPublicAPI?: boolean): Promise<any> => {
} }
const defaultOptions = { const defaultOptions = {
method: 'POST', method: 'POST',
url: `${urlPrefix}/files/upload`, url: url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`,
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
......
...@@ -103,6 +103,10 @@ export const fetchCurrentWorkspace: Fetcher<ICurrentWorkspace, { url: string; pa ...@@ -103,6 +103,10 @@ export const fetchCurrentWorkspace: Fetcher<ICurrentWorkspace, { url: string; pa
return get<ICurrentWorkspace>(url, { params }) return get<ICurrentWorkspace>(url, { params })
} }
export const updateCurrentWorkspace: Fetcher<ICurrentWorkspace, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<ICurrentWorkspace>(url, { body })
}
export const fetchWorkspaces: Fetcher<{ workspaces: IWorkspace[] }, { url: string; params: Record<string, any> }> = ({ url, params }) => { export const fetchWorkspaces: Fetcher<{ workspaces: IWorkspace[] }, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<{ workspaces: IWorkspace[] }>(url, { params }) return get<{ workspaces: IWorkspace[] }>(url, { params })
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment