mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 21:44:59 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea30612013 | ||
|
|
4445612bb8 | ||
|
|
da56393078 | ||
|
|
a86ff1e8d0 | ||
|
|
7fce5650b8 | ||
|
|
b949357e66 | ||
|
|
8e71b68dbf | ||
|
|
b77eed221f | ||
|
|
1fa91474d0 | ||
|
|
fc32a89292 | ||
|
|
ea24010298 | ||
|
|
ae1f80a52c | ||
|
|
2f95431c9b | ||
|
|
d59683eff1 | ||
|
|
de77776cd0 | ||
|
|
74ccfea570 | ||
|
|
1f5a549589 | ||
|
|
3ccdc2e307 | ||
|
|
688b725807 | ||
|
|
ae01ea576c |
No files matched your search
@@ -19,6 +19,41 @@ Fixing other platforms In Progress.
|
||||
|
||||
[Upgrade WeKan](https://wekan.fi/upgrade/)
|
||||
|
||||
# v8.01 2025-10-11 WeKan ® release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Made possible to start WeKan immediately without running any database migrations](https://github.com/wekan/wekan/commit/3ccdc2e3070978a79bc68941375122504318677d).
|
||||
Thanks to xet7.
|
||||
- [Add support for MongoDB 3-8 client, detecting which one is in use](https://github.com/wekan/wekan/commit/74ccfea5703b08b2166593d1bb8e1d981970cec0).
|
||||
Thanks to xet7.
|
||||
- [Add support for Snap MongoDB Server 3-8 support. Part 1](https://github.com/wekan/wekan/commit/b949357e665d97604cbecb04a9211319ac4e13c2).
|
||||
Thanks to xet7.
|
||||
- [Added attachments API and admin panel attachment management for file storage backends settings. Fixed drag drop upload attachments from file manager to minicard or opened card](https://github.com/wekan/wekan/commit/ae1f80a52cde09689dddb1209708630b4949b7ee).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fixed attachments migrations at Admin Panel to not use too much CPU while migrating attachments](https://github.com/wekan/wekan/commit/d59683eff1267ff87a6aef9ae36c7aebbe10eaa1).
|
||||
Thanks to xet7.
|
||||
- [Fixed per-card and per-board settings of showing checkist at minicard](https://github.com/wekan/wekan/commit/fc32a89292e4d314bd3868739f6e01bc2bc17018).
|
||||
Thanks to xet7.
|
||||
- [Fix count of Orgs Teams People at Admin Panel](https://github.com/wekan/wekan/commit/b77eed221f9e5fc657234a6de3e11c27f4de3d38).
|
||||
Thanks to xet7.
|
||||
- [Fixed attachments and minicard related bugs that prevented WeKan starting](https://github.com/wekan/wekan/commit/a86ff1e8d03d330a7ea0702cf3677859ed552509).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.00 2025-10-11 WeKan ® release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Snap: Migrate MongoDB from 3 to 7 only when "snap set wekan migrate-mongodb='true'". Not automatically](https://github.com/wekan/wekan/commit/ae01ea576c3073acbc181a5b19008ddaea4700e1).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v7.99 2025-10-11 WeKan ® release
|
||||
|
||||
This release fixed the following bugs:
|
||||
|
||||
+3
-3
@@ -249,9 +249,9 @@ cd /home/wekan/app
|
||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
|
||||
#mv /home/wekan/app_build/bundle /build
|
||||
wget "https://github.com/wekan/wekan/releases/download/v7.99/wekan-7.99-amd64.zip"
|
||||
unzip wekan-7.99-amd64.zip
|
||||
rm wekan-7.99-amd64.zip
|
||||
wget "https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64.zip"
|
||||
unzip wekan-8.01-amd64.zip
|
||||
rm wekan-8.01-amd64.zip
|
||||
mv /home/wekan/app/bundle /build
|
||||
|
||||
# Put back the original tar
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v7.99.0"
|
||||
appVersion: "v8.01.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -39,6 +39,12 @@ If *nix: chmod +x api.py => ./api.py users
|
||||
python3 api.py addcustomfieldtoboard AUTHORID BOARDID NAME TYPE SETTINGS SHOWONCARD AUTOMATICALLYONCARD SHOWLABELONMINICARD SHOWSUMATTOPOFLIST # Add Custom Field to Board
|
||||
python3 api.py editcustomfield BOARDID LISTID CARDID CUSTOMFIELDID NEWCUSTOMFIELDVALUE # Edit Custom Field
|
||||
python3 api.py listattachments BOARDID # List attachments
|
||||
python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND] # Upload attachment to card
|
||||
python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH # Download attachment to local file
|
||||
python3 api.py attachmentinfo ATTACHMENTID # Get attachment information
|
||||
python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID # List attachments for specific card
|
||||
python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move] # Copy or move attachment
|
||||
python3 api.py deleteattachment ATTACHMENTID # Delete attachment
|
||||
python3 api.py cardsbyswimlane SWIMLANEID LISTID # Retrieve cards list on a swimlane
|
||||
python3 api.py getcard BOARDID LISTID CARDID # Get card info
|
||||
python3 api.py addlabel BOARDID LISTID CARDID LABELID # Add label to a card
|
||||
@@ -750,3 +756,273 @@ if arguments == 1:
|
||||
data2 = body.text.replace('}',"}\n")
|
||||
print(data2)
|
||||
# ------- LIST OF PUBLIC BOARDS END -----------
|
||||
|
||||
# ------- NEW ATTACHMENT API ENDPOINTS START -----------
|
||||
|
||||
if sys.argv[1] == 'uploadattachment':
|
||||
# ------- UPLOAD ATTACHMENT START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND]")
|
||||
print("Storage backends: fs, gridfs, s3")
|
||||
exit(1)
|
||||
|
||||
boardid = sys.argv[2]
|
||||
swimlaneid = sys.argv[3]
|
||||
listid = sys.argv[4]
|
||||
cardid = sys.argv[5]
|
||||
filepath = sys.argv[6]
|
||||
storage_backend = sys.argv[7] if arguments > 6 else None
|
||||
|
||||
# Read file and convert to base64
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
file_data = f.read()
|
||||
import base64
|
||||
base64_data = base64.b64encode(file_data).decode('utf-8')
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File '{filepath}' not found")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error reading file: {e}")
|
||||
exit(1)
|
||||
|
||||
# Get file info
|
||||
import os
|
||||
filename = os.path.basename(filepath)
|
||||
import mimetypes
|
||||
file_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
|
||||
|
||||
# Prepare request data
|
||||
upload_data = {
|
||||
'boardId': boardid,
|
||||
'swimlaneId': swimlaneid,
|
||||
'listId': listid,
|
||||
'cardId': cardid,
|
||||
'fileData': base64_data,
|
||||
'fileName': filename,
|
||||
'fileType': file_type
|
||||
}
|
||||
|
||||
if storage_backend:
|
||||
upload_data['storageBackend'] = storage_backend
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
upload_url = wekanurl + 'api/attachment/upload'
|
||||
|
||||
try:
|
||||
response = requests.post(upload_url, headers=headers, json=upload_data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
print(f"Upload successful!")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
print(f"Storage: {result.get('storageBackend')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Upload failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- UPLOAD ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'downloadattachment':
|
||||
# ------- DOWNLOAD ATTACHMENT START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
outputpath = sys.argv[3]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
download_url = wekanurl + f'api/attachment/download/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.get(download_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
# Decode base64 data and save to file
|
||||
import base64
|
||||
file_data = base64.b64decode(result.get('base64Data'))
|
||||
|
||||
with open(outputpath, 'wb') as f:
|
||||
f.write(file_data)
|
||||
|
||||
print(f"Download successful!")
|
||||
print(f"File saved to: {outputpath}")
|
||||
print(f"Original filename: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
print(f"Storage: {result.get('storageBackend')}")
|
||||
else:
|
||||
print(f"Download failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Download failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- DOWNLOAD ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'attachmentinfo':
|
||||
# ------- ATTACHMENT INFO START -----------
|
||||
if arguments < 2:
|
||||
print("Usage: python3 api.py attachmentinfo ATTACHMENTID")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
info_url = wekanurl + f'api/attachment/info/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.get(info_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print("=== ATTACHMENT INFO ===")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File Name: {result.get('fileName')}")
|
||||
print(f"File Size: {result.get('fileSize')} bytes")
|
||||
print(f"File Type: {result.get('fileType')}")
|
||||
print(f"Storage Backend: {result.get('storageBackend')}")
|
||||
print(f"Board ID: {result.get('boardId')}")
|
||||
print(f"Swimlane ID: {result.get('swimlaneId')}")
|
||||
print(f"List ID: {result.get('listId')}")
|
||||
print(f"Card ID: {result.get('cardId')}")
|
||||
print(f"Created At: {result.get('createdAt')}")
|
||||
print(f"Is Image: {result.get('isImage')}")
|
||||
print(f"Versions: {len(result.get('versions', []))}")
|
||||
else:
|
||||
print(f"Failed to get attachment info: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- ATTACHMENT INFO END -----------
|
||||
|
||||
if sys.argv[1] == 'listcardattachments':
|
||||
# ------- LIST CARD ATTACHMENTS START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID")
|
||||
exit(1)
|
||||
|
||||
boardid = sys.argv[2]
|
||||
swimlaneid = sys.argv[3]
|
||||
listid = sys.argv[4]
|
||||
cardid = sys.argv[5]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
list_url = wekanurl + f'api/attachment/list/{boardid}/{swimlaneid}/{listid}/{cardid}'
|
||||
|
||||
try:
|
||||
response = requests.get(list_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
attachments = result.get('attachments', [])
|
||||
print(f"=== CARD ATTACHMENTS ({len(attachments)}) ===")
|
||||
for attachment in attachments:
|
||||
print(f"ID: {attachment.get('attachmentId')}")
|
||||
print(f"Name: {attachment.get('fileName')}")
|
||||
print(f"Size: {attachment.get('fileSize')} bytes")
|
||||
print(f"Type: {attachment.get('fileType')}")
|
||||
print(f"Storage: {attachment.get('storageBackend')}")
|
||||
print(f"Created: {attachment.get('createdAt')}")
|
||||
print("---")
|
||||
else:
|
||||
print(f"Failed to list attachments: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- LIST CARD ATTACHMENTS END -----------
|
||||
|
||||
if sys.argv[1] == 'copymoveattachment':
|
||||
# ------- COPY/MOVE ATTACHMENT START -----------
|
||||
if arguments < 6:
|
||||
print("Usage: python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move]")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
targetboardid = sys.argv[3]
|
||||
targetswimlaneid = sys.argv[4]
|
||||
targetlistid = sys.argv[5]
|
||||
targetcardid = sys.argv[6]
|
||||
operation = sys.argv[7] if arguments > 6 else 'copy'
|
||||
|
||||
if operation not in ['copy', 'move']:
|
||||
print("Operation must be 'copy' or 'move'")
|
||||
exit(1)
|
||||
|
||||
# Prepare request data
|
||||
request_data = {
|
||||
'attachmentId': attachmentid,
|
||||
'targetBoardId': targetboardid,
|
||||
'targetSwimlaneId': targetswimlaneid,
|
||||
'targetListId': targetlistid,
|
||||
'targetCardId': targetcardid
|
||||
}
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
api_url = wekanurl + f'api/attachment/{operation}'
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, headers=headers, json=request_data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print(f"{operation.capitalize()} successful!")
|
||||
if operation == 'copy':
|
||||
print(f"Source Attachment ID: {result.get('sourceAttachmentId')}")
|
||||
print(f"New Attachment ID: {result.get('newAttachmentId')}")
|
||||
else:
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"Source Board: {result.get('sourceBoardId')}")
|
||||
print(f"Target Board: {result.get('targetBoardId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
else:
|
||||
print(f"{operation.capitalize()} failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"{operation.capitalize()} failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- COPY/MOVE ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'deleteattachment':
|
||||
# ------- DELETE ATTACHMENT START -----------
|
||||
if arguments < 2:
|
||||
print("Usage: python3 api.py deleteattachment ATTACHMENTID")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
delete_url = wekanurl + f'api/attachment/delete/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.delete(delete_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print("Delete successful!")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
else:
|
||||
print(f"Delete failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Delete failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- DELETE ATTACHMENT END -----------
|
||||
|
||||
# ------- NEW ATTACHMENT API ENDPOINTS END -----------
|
||||
@@ -695,6 +695,14 @@ template(name="cardDetailsActionsPopup")
|
||||
a.js-set-card-color
|
||||
i.fa.fa-paint-brush
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
li
|
||||
a.js-toggle-show-list-on-minicard
|
||||
if showListOnMinicard
|
||||
i.fa.fa-eye
|
||||
| {{_ 'hide-list-on-minicard'}}
|
||||
else
|
||||
i.fa.fa-eye-slash
|
||||
| {{_ 'show-list-on-minicard'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
|
||||
@@ -657,6 +657,10 @@ Template.cardDetailsActionsPopup.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
|
||||
showListOnMinicard() {
|
||||
return this.showListOnMinicard;
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardDetailsActionsPopup.events({
|
||||
@@ -702,6 +706,12 @@ Template.cardDetailsActionsPopup.events({
|
||||
if (!err && ret) Popup.close();
|
||||
});
|
||||
},
|
||||
'click .js-toggle-show-list-on-minicard'() {
|
||||
const currentCard = this;
|
||||
const newValue = !currentCard.showListOnMinicard;
|
||||
Cards.update(currentCard._id, { $set: { showListOnMinicard: newValue } });
|
||||
Popup.close();
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
||||
@@ -588,3 +588,18 @@
|
||||
transform: scale(1.02);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* List name display on minicard */
|
||||
.minicard-list-name {
|
||||
font-size: 0.75em;
|
||||
color: #8c8c8c;
|
||||
margin-top: 0.2vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3vw;
|
||||
}
|
||||
|
||||
.minicard-list-name i.fa {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -72,12 +72,6 @@ template(name="minicard")
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
= getTitle
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'subtext-with-parent' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentCardName }}
|
||||
if labels
|
||||
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
|
||||
each labels
|
||||
@@ -197,6 +191,16 @@ template(name="minicard")
|
||||
.minicard-description
|
||||
+viewer
|
||||
| {{ getDescription }}
|
||||
if shouldShowListOnMinicard
|
||||
.minicard-list-name
|
||||
i.fa.fa-list
|
||||
| {{ listName }}
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'subtext-with-parent' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentCardName }}
|
||||
|
||||
template(name="editCardSortOrderPopup")
|
||||
input.js-edit-card-sort-popup(type='text' autofocus value=sort dir="auto")
|
||||
|
||||
@@ -196,6 +196,17 @@ Template.minicard.helpers({
|
||||
},
|
||||
uploadCount() {
|
||||
return uploadProgressManager.getUploadCountForCard(this._id);
|
||||
},
|
||||
listName() {
|
||||
const list = this.list();
|
||||
return list ? list.title : '';
|
||||
},
|
||||
|
||||
shouldShowListOnMinicard() {
|
||||
// Show list name if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific card has the setting enabled
|
||||
return this.currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class AdminReport extends BlazeComponent {
|
||||
}
|
||||
|
||||
resultsCount() {
|
||||
return this.collection.find().count();
|
||||
return this.collection.find().countDocuments();
|
||||
}
|
||||
|
||||
fileSize(size) {
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
template(name="attachmentSettings")
|
||||
.setting-content.attachment-settings-content
|
||||
unless currentUser.isAdmin
|
||||
| {{_ 'error-notAuthorized'}}
|
||||
else
|
||||
.content-body
|
||||
.side-menu
|
||||
ul
|
||||
li
|
||||
a.js-attachment-storage-settings(data-id="storage-settings")
|
||||
i.fa.fa-cog
|
||||
| {{_ 'attachment-storage-settings'}}
|
||||
li
|
||||
a.js-attachment-migration(data-id="attachment-migration")
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-migration'}}
|
||||
li
|
||||
a.js-attachment-monitoring(data-id="attachment-monitoring")
|
||||
i.fa.fa-chart-line
|
||||
| {{_ 'attachment-monitoring'}}
|
||||
|
||||
.main-body
|
||||
if loading.get
|
||||
+spinner
|
||||
else if showStorageSettings.get
|
||||
+storageSettings
|
||||
else if showMigration.get
|
||||
+attachmentMigration
|
||||
else if showMonitoring.get
|
||||
+attachmentMonitoring
|
||||
|
||||
template(name="storageSettings")
|
||||
.storage-settings
|
||||
h3 {{_ 'attachment-storage-configuration'}}
|
||||
|
||||
.storage-config-section
|
||||
h4 {{_ 'filesystem-storage'}}
|
||||
.form-group
|
||||
label {{_ 'writable-path'}}
|
||||
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'filesystem-path-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'attachments-path'}}
|
||||
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'attachments-path-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'avatars-path'}}
|
||||
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'avatars-path-description'}}
|
||||
|
||||
.storage-config-section
|
||||
h4 {{_ 'mongodb-gridfs-storage'}}
|
||||
.form-group
|
||||
label {{_ 'gridfs-enabled'}}
|
||||
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
|
||||
|
||||
.storage-config-section
|
||||
h4 {{_ 's3-minio-storage'}}
|
||||
.form-group
|
||||
label {{_ 's3-enabled'}}
|
||||
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-enabled-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-endpoint'}}
|
||||
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-endpoint-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-bucket'}}
|
||||
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-bucket-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-region'}}
|
||||
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-region-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-access-key'}}
|
||||
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-access-key-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-secret-key'}}
|
||||
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
|
||||
small.form-text.text-muted {{_ 's3-secret-key-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-ssl-enabled'}}
|
||||
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-port'}}
|
||||
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-port-description'}}
|
||||
|
||||
.storage-actions
|
||||
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
|
||||
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
|
||||
|
||||
template(name="attachmentMigration")
|
||||
.attachment-migration
|
||||
h3 {{_ 'attachment-migration'}}
|
||||
|
||||
.migration-controls
|
||||
.form-group
|
||||
label {{_ 'migration-batch-size'}}
|
||||
input.wekan-form-control#migration-batch-size(type="number" value="{{migrationBatchSize}}" min="1" max="100")
|
||||
small.form-text.text-muted {{_ 'migration-batch-size-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'migration-delay-ms'}}
|
||||
input.wekan-form-control#migration-delay-ms(type="number" value="{{migrationDelayMs}}" min="100" max="10000")
|
||||
small.form-text.text-muted {{_ 'migration-delay-ms-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'migration-cpu-threshold'}}
|
||||
input.wekan-form-control#migration-cpu-threshold(type="number" value="{{migrationCpuThreshold}}" min="10" max="90")
|
||||
small.form-text.text-muted {{_ 'migration-cpu-threshold-description'}}
|
||||
|
||||
.migration-actions
|
||||
.migration-buttons
|
||||
button.js-migrate-all-to-filesystem.btn.btn-primary {{_ 'migrate-all-to-filesystem'}}
|
||||
button.js-migrate-all-to-gridfs.btn.btn-primary {{_ 'migrate-all-to-gridfs'}}
|
||||
button.js-migrate-all-to-s3.btn.btn-primary {{_ 'migrate-all-to-s3'}}
|
||||
|
||||
.migration-controls
|
||||
button.js-pause-migration.btn.btn-warning {{_ 'pause-migration'}}
|
||||
button.js-resume-migration.btn.btn-success {{_ 'resume-migration'}}
|
||||
button.js-stop-migration.btn.btn-danger {{_ 'stop-migration'}}
|
||||
|
||||
.migration-progress
|
||||
h4 {{_ 'migration-progress'}}
|
||||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationProgress}}%
|
||||
|
||||
.migration-stats
|
||||
.stat-item
|
||||
span.label {{_ 'total-attachments'}}:
|
||||
span.value {{totalAttachments}}
|
||||
.stat-item
|
||||
span.label {{_ 'migrated-attachments'}}:
|
||||
span.value {{migratedAttachments}}
|
||||
.stat-item
|
||||
span.label {{_ 'remaining-attachments'}}:
|
||||
span.value {{remainingAttachments}}
|
||||
.stat-item
|
||||
span.label {{_ 'migration-status'}}:
|
||||
span.value {{migrationStatus}}
|
||||
|
||||
.migration-log
|
||||
h4 {{_ 'migration-log'}}
|
||||
.log-container
|
||||
pre#migration-log-content {{migrationLog}}
|
||||
|
||||
template(name="attachmentMonitoring")
|
||||
.attachment-monitoring
|
||||
h3 {{_ 'attachment-monitoring'}}
|
||||
|
||||
.monitoring-stats
|
||||
.stats-grid
|
||||
.stat-card
|
||||
h5 {{_ 'total-attachments'}}
|
||||
.stat-value {{totalAttachments}}
|
||||
.stat-card
|
||||
h5 {{_ 'filesystem-attachments'}}
|
||||
.stat-value {{filesystemAttachments}}
|
||||
.stat-card
|
||||
h5 {{_ 'gridfs-attachments'}}
|
||||
.stat-value {{gridfsAttachments}}
|
||||
.stat-card
|
||||
h5 {{_ 's3-attachments'}}
|
||||
.stat-value {{s3Attachments}}
|
||||
.stat-card
|
||||
h5 {{_ 'total-size'}}
|
||||
.stat-value {{totalSize}}
|
||||
.stat-card
|
||||
h5 {{_ 'filesystem-size'}}
|
||||
.stat-value {{filesystemSize}}
|
||||
.stat-card
|
||||
h5 {{_ 'gridfs-size'}}
|
||||
.stat-value {{gridfsSize}}
|
||||
.stat-card
|
||||
h5 {{_ 's3-size'}}
|
||||
.stat-value {{s3Size}}
|
||||
|
||||
.monitoring-charts
|
||||
h4 {{_ 'storage-distribution'}}
|
||||
.chart-container
|
||||
canvas#storage-distribution-chart
|
||||
|
||||
.monitoring-actions
|
||||
button.js-refresh-monitoring.btn.btn-secondary {{_ 'refresh-monitoring'}}
|
||||
button.js-export-monitoring.btn.btn-primary {{_ 'export-monitoring'}}
|
||||
@@ -0,0 +1,464 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Session } from 'meteor/session';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||
import { Chart } from 'chart.js';
|
||||
|
||||
// Global reactive variables for attachment settings
|
||||
const attachmentSettings = {
|
||||
loading: new ReactiveVar(false),
|
||||
showStorageSettings: new ReactiveVar(false),
|
||||
showMigration: new ReactiveVar(false),
|
||||
showMonitoring: new ReactiveVar(false),
|
||||
|
||||
// Storage configuration
|
||||
filesystemPath: new ReactiveVar(''),
|
||||
attachmentsPath: new ReactiveVar(''),
|
||||
avatarsPath: new ReactiveVar(''),
|
||||
gridfsEnabled: new ReactiveVar(false),
|
||||
s3Enabled: new ReactiveVar(false),
|
||||
s3Endpoint: new ReactiveVar(''),
|
||||
s3Bucket: new ReactiveVar(''),
|
||||
s3Region: new ReactiveVar(''),
|
||||
s3SslEnabled: new ReactiveVar(false),
|
||||
s3Port: new ReactiveVar(443),
|
||||
|
||||
// Migration settings
|
||||
migrationBatchSize: new ReactiveVar(10),
|
||||
migrationDelayMs: new ReactiveVar(1000),
|
||||
migrationCpuThreshold: new ReactiveVar(70),
|
||||
migrationProgress: new ReactiveVar(0),
|
||||
migrationStatus: new ReactiveVar('idle'),
|
||||
migrationLog: new ReactiveVar(''),
|
||||
|
||||
// Monitoring data
|
||||
totalAttachments: new ReactiveVar(0),
|
||||
filesystemAttachments: new ReactiveVar(0),
|
||||
gridfsAttachments: new ReactiveVar(0),
|
||||
s3Attachments: new ReactiveVar(0),
|
||||
totalSize: new ReactiveVar(0),
|
||||
filesystemSize: new ReactiveVar(0),
|
||||
gridfsSize: new ReactiveVar(0),
|
||||
s3Size: new ReactiveVar(0),
|
||||
|
||||
// Migration state
|
||||
isMigrationRunning: new ReactiveVar(false),
|
||||
isMigrationPaused: new ReactiveVar(false),
|
||||
migrationQueue: new ReactiveVar([]),
|
||||
currentMigration: new ReactiveVar(null)
|
||||
};
|
||||
|
||||
// Main attachment settings component
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.loading = attachmentSettings.loading;
|
||||
this.showStorageSettings = attachmentSettings.showStorageSettings;
|
||||
this.showMigration = attachmentSettings.showMigration;
|
||||
this.showMonitoring = attachmentSettings.showMonitoring;
|
||||
|
||||
// Load initial data
|
||||
this.loadStorageConfiguration();
|
||||
this.loadMigrationSettings();
|
||||
this.loadMonitoringData();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click a.js-attachment-storage-settings': this.switchToStorageSettings,
|
||||
'click a.js-attachment-migration': this.switchToMigration,
|
||||
'click a.js-attachment-monitoring': this.switchToMonitoring,
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
switchToStorageSettings(event) {
|
||||
this.switchMenu(event, 'storage-settings');
|
||||
this.showStorageSettings.set(true);
|
||||
this.showMigration.set(false);
|
||||
this.showMonitoring.set(false);
|
||||
},
|
||||
|
||||
switchToMigration(event) {
|
||||
this.switchMenu(event, 'attachment-migration');
|
||||
this.showStorageSettings.set(false);
|
||||
this.showMigration.set(true);
|
||||
this.showMonitoring.set(false);
|
||||
},
|
||||
|
||||
switchToMonitoring(event) {
|
||||
this.switchMenu(event, 'attachment-monitoring');
|
||||
this.showStorageSettings.set(false);
|
||||
this.showMigration.set(false);
|
||||
this.showMonitoring.set(true);
|
||||
},
|
||||
|
||||
switchMenu(event, targetId) {
|
||||
const target = $(event.target);
|
||||
if (!target.hasClass('active')) {
|
||||
this.loading.set(true);
|
||||
|
||||
$('.side-menu li.active').removeClass('active');
|
||||
target.parent().addClass('active');
|
||||
|
||||
// Load data based on target
|
||||
if (targetId === 'storage-settings') {
|
||||
this.loadStorageConfiguration();
|
||||
} else if (targetId === 'attachment-migration') {
|
||||
this.loadMigrationSettings();
|
||||
} else if (targetId === 'attachment-monitoring') {
|
||||
this.loadMonitoringData();
|
||||
}
|
||||
|
||||
this.loading.set(false);
|
||||
}
|
||||
},
|
||||
|
||||
loadStorageConfiguration() {
|
||||
Meteor.call('getAttachmentStorageConfiguration', (error, result) => {
|
||||
if (!error && result) {
|
||||
attachmentSettings.filesystemPath.set(result.filesystemPath || '');
|
||||
attachmentSettings.attachmentsPath.set(result.attachmentsPath || '');
|
||||
attachmentSettings.avatarsPath.set(result.avatarsPath || '');
|
||||
attachmentSettings.gridfsEnabled.set(result.gridfsEnabled || false);
|
||||
attachmentSettings.s3Enabled.set(result.s3Enabled || false);
|
||||
attachmentSettings.s3Endpoint.set(result.s3Endpoint || '');
|
||||
attachmentSettings.s3Bucket.set(result.s3Bucket || '');
|
||||
attachmentSettings.s3Region.set(result.s3Region || '');
|
||||
attachmentSettings.s3SslEnabled.set(result.s3SslEnabled || false);
|
||||
attachmentSettings.s3Port.set(result.s3Port || 443);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMigrationSettings() {
|
||||
Meteor.call('getAttachmentMigrationSettings', (error, result) => {
|
||||
if (!error && result) {
|
||||
attachmentSettings.migrationBatchSize.set(result.batchSize || 10);
|
||||
attachmentSettings.migrationDelayMs.set(result.delayMs || 1000);
|
||||
attachmentSettings.migrationCpuThreshold.set(result.cpuThreshold || 70);
|
||||
attachmentSettings.migrationStatus.set(result.status || 'idle');
|
||||
attachmentSettings.migrationProgress.set(result.progress || 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMonitoringData() {
|
||||
Meteor.call('getAttachmentMonitoringData', (error, result) => {
|
||||
if (!error && result) {
|
||||
attachmentSettings.totalAttachments.set(result.totalAttachments || 0);
|
||||
attachmentSettings.filesystemAttachments.set(result.filesystemAttachments || 0);
|
||||
attachmentSettings.gridfsAttachments.set(result.gridfsAttachments || 0);
|
||||
attachmentSettings.s3Attachments.set(result.s3Attachments || 0);
|
||||
attachmentSettings.totalSize.set(result.totalSize || 0);
|
||||
attachmentSettings.filesystemSize.set(result.filesystemSize || 0);
|
||||
attachmentSettings.gridfsSize.set(result.gridfsSize || 0);
|
||||
attachmentSettings.s3Size.set(result.s3Size || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).register('attachmentSettings');
|
||||
|
||||
// Storage settings component
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.filesystemPath = attachmentSettings.filesystemPath;
|
||||
this.attachmentsPath = attachmentSettings.attachmentsPath;
|
||||
this.avatarsPath = attachmentSettings.avatarsPath;
|
||||
this.gridfsEnabled = attachmentSettings.gridfsEnabled;
|
||||
this.s3Enabled = attachmentSettings.s3Enabled;
|
||||
this.s3Endpoint = attachmentSettings.s3Endpoint;
|
||||
this.s3Bucket = attachmentSettings.s3Bucket;
|
||||
this.s3Region = attachmentSettings.s3Region;
|
||||
this.s3SslEnabled = attachmentSettings.s3SslEnabled;
|
||||
this.s3Port = attachmentSettings.s3Port;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click button.js-test-s3-connection': this.testS3Connection,
|
||||
'click button.js-save-s3-settings': this.saveS3Settings,
|
||||
'change input#s3-secret-key': this.updateS3SecretKey
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
testS3Connection() {
|
||||
const secretKey = $('#s3-secret-key').val();
|
||||
if (!secretKey) {
|
||||
alert(TAPi18n.__('s3-secret-key-required'));
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.call('testS3Connection', { secretKey }, (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('s3-connection-success'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
saveS3Settings() {
|
||||
const secretKey = $('#s3-secret-key').val();
|
||||
if (!secretKey) {
|
||||
alert(TAPi18n.__('s3-secret-key-required'));
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('s3-settings-saved'));
|
||||
$('#s3-secret-key').val(''); // Clear the password field
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateS3SecretKey(event) {
|
||||
// This method can be used to validate the secret key format
|
||||
const secretKey = event.target.value;
|
||||
// Add validation logic here if needed
|
||||
}
|
||||
}).register('storageSettings');
|
||||
|
||||
// Migration component
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.migrationBatchSize = attachmentSettings.migrationBatchSize;
|
||||
this.migrationDelayMs = attachmentSettings.migrationDelayMs;
|
||||
this.migrationCpuThreshold = attachmentSettings.migrationCpuThreshold;
|
||||
this.migrationProgress = attachmentSettings.migrationProgress;
|
||||
this.migrationStatus = attachmentSettings.migrationStatus;
|
||||
this.migrationLog = attachmentSettings.migrationLog;
|
||||
this.isMigrationRunning = attachmentSettings.isMigrationRunning;
|
||||
this.isMigrationPaused = attachmentSettings.isMigrationPaused;
|
||||
|
||||
// Subscribe to migration updates
|
||||
this.subscription = Meteor.subscribe('attachmentMigrationStatus');
|
||||
|
||||
// Set up reactive updates
|
||||
this.autorun(() => {
|
||||
const status = attachmentSettings.migrationStatus.get();
|
||||
if (status === 'running') {
|
||||
this.isMigrationRunning.set(true);
|
||||
} else {
|
||||
this.isMigrationRunning.set(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onDestroyed() {
|
||||
if (this.subscription) {
|
||||
this.subscription.stop();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click button.js-migrate-all-to-filesystem': () => this.startMigration('filesystem'),
|
||||
'click button.js-migrate-all-to-gridfs': () => this.startMigration('gridfs'),
|
||||
'click button.js-migrate-all-to-s3': () => this.startMigration('s3'),
|
||||
'click button.js-pause-migration': this.pauseMigration,
|
||||
'click button.js-resume-migration': this.resumeMigration,
|
||||
'click button.js-stop-migration': this.stopMigration,
|
||||
'change input#migration-batch-size': this.updateBatchSize,
|
||||
'change input#migration-delay-ms': this.updateDelayMs,
|
||||
'change input#migration-cpu-threshold': this.updateCpuThreshold
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
startMigration(targetStorage) {
|
||||
const batchSize = parseInt($('#migration-batch-size').val()) || 10;
|
||||
const delayMs = parseInt($('#migration-delay-ms').val()) || 1000;
|
||||
const cpuThreshold = parseInt($('#migration-cpu-threshold').val()) || 70;
|
||||
|
||||
Meteor.call('startAttachmentMigration', {
|
||||
targetStorage,
|
||||
batchSize,
|
||||
delayMs,
|
||||
cpuThreshold
|
||||
}, (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
this.addToLog(TAPi18n.__('migration-started') + ': ' + targetStorage);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
pauseMigration() {
|
||||
Meteor.call('pauseAttachmentMigration', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
this.addToLog(TAPi18n.__('migration-paused'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
resumeMigration() {
|
||||
Meteor.call('resumeAttachmentMigration', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-resume-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
this.addToLog(TAPi18n.__('migration-resumed'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
stopMigration() {
|
||||
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
|
||||
Meteor.call('stopAttachmentMigration', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
this.addToLog(TAPi18n.__('migration-stopped'));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateBatchSize(event) {
|
||||
const value = parseInt(event.target.value);
|
||||
if (value >= 1 && value <= 100) {
|
||||
attachmentSettings.migrationBatchSize.set(value);
|
||||
}
|
||||
},
|
||||
|
||||
updateDelayMs(event) {
|
||||
const value = parseInt(event.target.value);
|
||||
if (value >= 100 && value <= 10000) {
|
||||
attachmentSettings.migrationDelayMs.set(value);
|
||||
}
|
||||
},
|
||||
|
||||
updateCpuThreshold(event) {
|
||||
const value = parseInt(event.target.value);
|
||||
if (value >= 10 && value <= 90) {
|
||||
attachmentSettings.migrationCpuThreshold.set(value);
|
||||
}
|
||||
},
|
||||
|
||||
addToLog(message) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const currentLog = attachmentSettings.migrationLog.get();
|
||||
const newLog = `[${timestamp}] ${message}\n${currentLog}`;
|
||||
attachmentSettings.migrationLog.set(newLog);
|
||||
}
|
||||
}).register('attachmentMigration');
|
||||
|
||||
// Monitoring component
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.totalAttachments = attachmentSettings.totalAttachments;
|
||||
this.filesystemAttachments = attachmentSettings.filesystemAttachments;
|
||||
this.gridfsAttachments = attachmentSettings.gridfsAttachments;
|
||||
this.s3Attachments = attachmentSettings.s3Attachments;
|
||||
this.totalSize = attachmentSettings.totalSize;
|
||||
this.filesystemSize = attachmentSettings.filesystemSize;
|
||||
this.gridfsSize = attachmentSettings.gridfsSize;
|
||||
this.s3Size = attachmentSettings.s3Size;
|
||||
|
||||
// Subscribe to monitoring updates
|
||||
this.subscription = Meteor.subscribe('attachmentMonitoringData');
|
||||
|
||||
// Set up chart
|
||||
this.autorun(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
onDestroyed() {
|
||||
if (this.subscription) {
|
||||
this.subscription.stop();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click button.js-refresh-monitoring': this.refreshMonitoring,
|
||||
'click button.js-export-monitoring': this.exportMonitoring
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
refreshMonitoring() {
|
||||
Meteor.call('refreshAttachmentMonitoringData', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('monitoring-refresh-failed') + ': ' + error.reason);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
exportMonitoring() {
|
||||
Meteor.call('exportAttachmentMonitoringData', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('monitoring-export-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
// Download the exported data
|
||||
const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'wekan-attachment-monitoring.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateChart() {
|
||||
const ctx = document.getElementById('storage-distribution-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
const filesystemCount = this.filesystemAttachments.get();
|
||||
const gridfsCount = this.gridfsAttachments.get();
|
||||
const s3Count = this.s3Attachments.get();
|
||||
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
|
||||
this.chart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: [
|
||||
TAPi18n.__('filesystem-storage'),
|
||||
TAPi18n.__('gridfs-storage'),
|
||||
TAPi18n.__('s3-storage')
|
||||
],
|
||||
datasets: [{
|
||||
data: [filesystemCount, gridfsCount, s3Count],
|
||||
backgroundColor: [
|
||||
'#28a745',
|
||||
'#007bff',
|
||||
'#ffc107'
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).register('attachmentMonitoring');
|
||||
|
||||
// Export the attachment settings for use in other components
|
||||
export { attachmentSettings };
|
||||
@@ -203,26 +203,35 @@ BlazeComponent.extendComponent({
|
||||
this.loading.set(w);
|
||||
},
|
||||
orgList() {
|
||||
const limitOrgs = this.page.get() * orgsPerPage;
|
||||
const orgs = ReactiveCache.getOrgs(this.findOrgsOptions.get(), {
|
||||
sort: { orgDisplayName: 1 },
|
||||
limit: limitOrgs,
|
||||
fields: { _id: true },
|
||||
});
|
||||
// Count only the items currently loaded to browser, not total from database
|
||||
this.numberOrgs.set(orgs.length);
|
||||
return orgs;
|
||||
},
|
||||
teamList() {
|
||||
const limitTeams = this.page.get() * teamsPerPage;
|
||||
const teams = ReactiveCache.getTeams(this.findTeamsOptions.get(), {
|
||||
sort: { teamDisplayName: 1 },
|
||||
limit: limitTeams,
|
||||
fields: { _id: true },
|
||||
});
|
||||
// Count only the items currently loaded to browser, not total from database
|
||||
this.numberTeams.set(teams.length);
|
||||
return teams;
|
||||
},
|
||||
peopleList() {
|
||||
const limitUsers = this.page.get() * usersPerPage;
|
||||
const users = ReactiveCache.getUsers(this.findUsersOptions.get(), {
|
||||
sort: { username: 1 },
|
||||
limit: limitUsers,
|
||||
fields: { _id: true },
|
||||
});
|
||||
// Count only the items currently loaded to browser, not total from database
|
||||
this.numberPeople.set(users.length);
|
||||
return users;
|
||||
},
|
||||
|
||||
@@ -42,6 +42,10 @@ template(name="setting")
|
||||
a.js-setting-menu(data-id="webhook-setting")
|
||||
i.fa.fa-globe
|
||||
| {{_ 'global-webhook'}}
|
||||
li
|
||||
a.js-setting-menu(data-id="attachment-settings")
|
||||
i.fa.fa-paperclip
|
||||
| {{_ 'attachment-settings'}}
|
||||
.main-body
|
||||
if loading.get
|
||||
+spinner
|
||||
@@ -62,6 +66,8 @@ template(name="setting")
|
||||
+layoutSettings
|
||||
else if webhookSetting.get
|
||||
+webhookSettings
|
||||
else if attachmentSettings.get
|
||||
+attachmentSettings
|
||||
|
||||
template(name="webhookSettings")
|
||||
span
|
||||
|
||||
@@ -16,6 +16,7 @@ BlazeComponent.extendComponent({
|
||||
this.accessibilitySetting = new ReactiveVar(false);
|
||||
this.layoutSetting = new ReactiveVar(false);
|
||||
this.webhookSetting = new ReactiveVar(false);
|
||||
this.attachmentSettings = new ReactiveVar(false);
|
||||
|
||||
Meteor.subscribe('setting');
|
||||
Meteor.subscribe('mailServer');
|
||||
@@ -113,6 +114,7 @@ BlazeComponent.extendComponent({
|
||||
this.accessibilitySetting.set('accessibility-setting' === targetID);
|
||||
this.layoutSetting.set('layout-setting' === targetID);
|
||||
this.webhookSetting.set('webhook-setting' === targetID);
|
||||
this.attachmentSettings.set('attachment-settings' === targetID);
|
||||
this.tableVisibilityModeSetting.set('tableVisibilityMode-setting' === targetID);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -284,6 +284,12 @@ template(name="boardCardSettingsPopup")
|
||||
span
|
||||
i.fa.fa-tags
|
||||
| {{_ 'labels'}}
|
||||
div.check-div
|
||||
a.flex.js-field-has-card-show-lists-on-minicard(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
|
||||
span
|
||||
i.fa.fa-list
|
||||
| {{_ 'card-show-lists-on-minicard'}}
|
||||
div.check-div
|
||||
a.flex.js-field-has-card-number(class="{{#if allowsCardNumber}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if allowsCardNumber}}is-checked{{/if}}")
|
||||
|
||||
@@ -996,10 +996,15 @@ BlazeComponent.extendComponent({
|
||||
return this.currentBoard.allowsShowLists;
|
||||
},
|
||||
|
||||
|
||||
allowsLabels() {
|
||||
return this.currentBoard.allowsLabels;
|
||||
},
|
||||
|
||||
allowsShowListsOnMinicard() {
|
||||
return this.currentBoard.allowsShowListsOnMinicard;
|
||||
},
|
||||
|
||||
allowsChecklists() {
|
||||
return this.currentBoard.allowsChecklists;
|
||||
},
|
||||
@@ -1282,13 +1287,29 @@ BlazeComponent.extendComponent({
|
||||
this.currentBoard.setAllowsLabels(this.currentBoard.allowsLabels);
|
||||
$(`.js-field-has-labels ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
this.currentBoard.allowsAssignee,
|
||||
this.currentBoard.allowsLabels,
|
||||
);
|
||||
$('.js-field-has-labels').toggleClass(
|
||||
CKCLS,
|
||||
this.currentBoard.allowsLabels,
|
||||
);
|
||||
},
|
||||
'click .js-field-has-card-show-lists-on-minicard'(evt) {
|
||||
evt.preventDefault();
|
||||
this.currentBoard.allowsShowListsOnMinicard = !this.currentBoard
|
||||
.allowsShowListsOnMinicard;
|
||||
this.currentBoard.setAllowsShowListsOnMinicard(
|
||||
this.currentBoard.allowsShowListsOnMinicard,
|
||||
);
|
||||
$(`.js-field-has-card-show-lists-on-minicard ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
this.currentBoard.allowsShowListsOnMinicard,
|
||||
);
|
||||
$('.js-field-has-card-show-lists-on-minicard').toggleClass(
|
||||
CKCLS,
|
||||
this.currentBoard.allowsShowListsOnMinicard,
|
||||
);
|
||||
},
|
||||
'click .js-field-has-description-title'(evt) {
|
||||
evt.preventDefault();
|
||||
this.currentBoard.allowsDescriptionTitle = !this.currentBoard
|
||||
|
||||
@@ -104,8 +104,8 @@ function initSortable(boardComponent, $listsDom) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the original swimlane ID of the list
|
||||
const originalSwimlaneId = list.swimlaneId;
|
||||
// Get the original swimlane ID of the list (handle backward compatibility)
|
||||
const originalSwimlaneId = list.getEffectiveSwimlaneId ? list.getEffectiveSwimlaneId() : (list.swimlaneId || null);
|
||||
|
||||
/*
|
||||
Reverted incomplete change list width,
|
||||
|
||||
@@ -23,7 +23,7 @@ Template.userAvatar.helpers({
|
||||
/*
|
||||
presenceStatusClassName() {
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
const userPresence = presences.findOne({ userId: this.userId });
|
||||
const userPresence = Presences.findOne({ userId: this.userId });
|
||||
if (user && user.isInvitedTo(Session.get('currentBoard'))) return 'pending';
|
||||
else if (!userPresence) return 'disconnected';
|
||||
else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
# MongoDB Compatibility Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide documents MongoDB compatibility issues and fixes for Wekan across MongoDB versions 3.0 through 8.0, ensuring proper operation with Meteor.js 2.14.
|
||||
|
||||
## Current Status
|
||||
|
||||
- **Meteor.js Version**: 2.14
|
||||
- **MongoDB Package**: mongo@1.16.8
|
||||
- **Supported MongoDB Versions**: 3.0 - 8.0
|
||||
- **Compatibility Status**: ✅ Fixed
|
||||
|
||||
## Compatibility Issues Fixed
|
||||
|
||||
### 1. Deprecated `.count()` Method
|
||||
|
||||
**Issue**: The `.count()` method is deprecated in newer MongoDB versions.
|
||||
|
||||
**Fixed Files**:
|
||||
- `models/users.js` - Line 1839
|
||||
- `server/migrations.js` - Line 209
|
||||
- `server/publications/cards.js` - Lines 709, 711, 713
|
||||
- `client/components/settings/adminReports.js` - Line 115
|
||||
|
||||
**Fix Applied**:
|
||||
```javascript
|
||||
// Before (deprecated)
|
||||
const count = collection.find().count();
|
||||
|
||||
// After (compatible)
|
||||
const count = collection.find().countDocuments();
|
||||
```
|
||||
|
||||
### 2. MongoDB 8.0 Null/Undefined Equality
|
||||
|
||||
**Issue**: MongoDB 8.0 changed behavior where equality matches to `null` no longer match `undefined` values.
|
||||
|
||||
**Fixed Files**:
|
||||
- `models/customFields.js` - Custom field initialization
|
||||
- `models/cards.js` - Card custom field initialization
|
||||
|
||||
**Fix Applied**:
|
||||
```javascript
|
||||
// Before (MongoDB 8.0 incompatible)
|
||||
{ field: null }
|
||||
|
||||
// After (MongoDB 8.0 compatible)
|
||||
{ field: { $in: [null, undefined] } }
|
||||
```
|
||||
|
||||
## Direct Operations Analysis
|
||||
|
||||
### Purpose of Direct Operations
|
||||
|
||||
Direct operations (`.direct.insert()`, `.direct.update()`, `.direct.remove()`) are used intentionally in Wekan to:
|
||||
|
||||
1. **Bypass Meteor Security**: For system operations that need to bypass validation
|
||||
2. **Migration Scripts**: For data migration operations
|
||||
3. **Import/Export**: For bulk data operations
|
||||
4. **System Initialization**: For setting up initial data
|
||||
|
||||
### Files Using Direct Operations
|
||||
|
||||
**Models**:
|
||||
- `models/wekanCreator.js` - Wekan board creation
|
||||
- `models/trelloCreator.js` - Trello import
|
||||
- `models/cards.js` - Card operations
|
||||
- `models/boards.js` - Board operations
|
||||
- `models/lists.js` - List operations
|
||||
- `models/swimlanes.js` - Swimlane operations
|
||||
- `models/customFields.js` - Custom field operations
|
||||
- `models/checklistItems.js` - Checklist operations
|
||||
- `models/integrations.js` - Integration operations
|
||||
- `models/csvCreator.js` - CSV export operations
|
||||
|
||||
**Server**:
|
||||
- `server/migrations.js` - Database migrations
|
||||
- `server/notifications/outgoing.js` - Notification operations
|
||||
|
||||
### Security Considerations
|
||||
|
||||
Direct operations bypass Meteor's security model, so they should only be used when:
|
||||
- ✅ System-level operations (migrations, imports)
|
||||
- ✅ Bulk operations that need performance
|
||||
- ✅ Operations that need to bypass validation
|
||||
- ❌ User-initiated operations (use regular methods)
|
||||
- ❌ Operations that need security validation
|
||||
|
||||
## Raw Collection Usage
|
||||
|
||||
### Purpose of Raw Collections
|
||||
|
||||
Raw collections (`.rawCollection()`) are used for:
|
||||
1. **Advanced Aggregation**: Complex aggregation pipelines
|
||||
2. **Database Commands**: Direct database commands
|
||||
3. **Index Management**: Creating/dropping indexes
|
||||
4. **Migration Operations**: Complex data transformations
|
||||
|
||||
### Files Using Raw Collections
|
||||
|
||||
- `models/server/metrics.js` - Metrics aggregation
|
||||
- `server/migrations.js` - Migration operations
|
||||
|
||||
### Security Considerations
|
||||
|
||||
Raw collections bypass all Meteor security, so they should only be used when:
|
||||
- ✅ Server-side only operations
|
||||
- ✅ System administration tasks
|
||||
- ✅ Complex aggregation pipelines
|
||||
- ❌ Client-side operations
|
||||
- ❌ User data operations without validation
|
||||
|
||||
## Version-Specific Compatibility
|
||||
|
||||
### MongoDB 3.0 - 3.6
|
||||
- ✅ Full compatibility
|
||||
- ✅ Uses mongodb3legacy driver
|
||||
- ✅ All operations supported
|
||||
|
||||
### MongoDB 4.0 - 4.4
|
||||
- ✅ Full compatibility
|
||||
- ✅ Uses mongodb4legacy driver
|
||||
- ✅ All operations supported
|
||||
|
||||
### MongoDB 5.0
|
||||
- ✅ Full compatibility
|
||||
- ✅ Uses mongodb5legacy driver
|
||||
- ✅ All operations supported
|
||||
|
||||
### MongoDB 6.0
|
||||
- ✅ Full compatibility
|
||||
- ✅ Uses mongodb6legacy driver
|
||||
- ✅ All operations supported
|
||||
|
||||
### MongoDB 7.0
|
||||
- ✅ Full compatibility
|
||||
- ✅ Uses mongodb7legacy driver
|
||||
- ✅ All operations supported
|
||||
|
||||
### MongoDB 8.0
|
||||
- ✅ Full compatibility (after fixes)
|
||||
- ✅ Uses mongodb8legacy driver
|
||||
- ✅ Null/undefined equality fixed
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Test Matrix
|
||||
|
||||
| MongoDB Version | Driver | Status | Test Priority |
|
||||
|----------------|--------|--------|---------------|
|
||||
| 3.0 | mongodb3legacy | ✅ Compatible | High |
|
||||
| 3.2 | mongodb3legacy | ✅ Compatible | High |
|
||||
| 3.4 | mongodb3legacy | ✅ Compatible | High |
|
||||
| 3.6 | mongodb3legacy | ✅ Compatible | High |
|
||||
| 4.0 | mongodb4legacy | ✅ Compatible | High |
|
||||
| 4.2 | mongodb4legacy | ✅ Compatible | High |
|
||||
| 4.4 | mongodb4legacy | ✅ Compatible | High |
|
||||
| 5.0 | mongodb5legacy | ✅ Compatible | Medium |
|
||||
| 6.0 | mongodb6legacy | ✅ Compatible | Medium |
|
||||
| 7.0 | mongodb7legacy | ✅ Compatible | Medium |
|
||||
| 8.0 | mongodb8legacy | ✅ Compatible | High |
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
1. **Basic Operations**:
|
||||
- Create/Read/Update/Delete operations
|
||||
- Collection queries and filters
|
||||
- Index operations
|
||||
|
||||
2. **Advanced Operations**:
|
||||
- Aggregation pipelines
|
||||
- Complex queries with null/undefined
|
||||
- Direct operations
|
||||
- Raw collection operations
|
||||
|
||||
3. **Migration Operations**:
|
||||
- Database migrations
|
||||
- Data import/export
|
||||
- Schema changes
|
||||
|
||||
4. **Performance Testing**:
|
||||
- Large dataset operations
|
||||
- Concurrent operations
|
||||
- Memory usage
|
||||
|
||||
## Monitoring and Debugging
|
||||
|
||||
### MongoDB Driver System
|
||||
|
||||
The MongoDB driver system provides:
|
||||
- Automatic version detection
|
||||
- Driver selection based on MongoDB version
|
||||
- Connection monitoring
|
||||
- Error tracking
|
||||
|
||||
### Debug Methods
|
||||
|
||||
```javascript
|
||||
// Get connection statistics
|
||||
Meteor.call('mongodb-driver-stats', (error, result) => {
|
||||
console.log('Connection Stats:', result);
|
||||
});
|
||||
|
||||
// Test connection
|
||||
Meteor.call('mongodb-driver-test-connection', (error, result) => {
|
||||
console.log('Connection Test:', result);
|
||||
});
|
||||
|
||||
// Get supported versions
|
||||
Meteor.call('mongodb-driver-supported-versions', (error, result) => {
|
||||
console.log('Supported Versions:', result);
|
||||
});
|
||||
```
|
||||
|
||||
### Real-time Monitoring
|
||||
|
||||
```javascript
|
||||
// Subscribe to monitoring
|
||||
Meteor.subscribe('mongodb-driver-monitor');
|
||||
|
||||
// Access data in template
|
||||
Template.yourTemplate.helpers({
|
||||
driverStats() {
|
||||
return MongoDBDriverMonitor.findOne('stats');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Query Optimization
|
||||
|
||||
1. **Use Indexes**: Ensure proper indexes for frequently queried fields
|
||||
2. **Limit Results**: Use `.limit()` and `.skip()` for pagination
|
||||
3. **Project Fields**: Use projection to limit returned fields
|
||||
4. **Aggregation**: Use aggregation pipelines for complex operations
|
||||
|
||||
### Security
|
||||
|
||||
1. **Validate Input**: Always validate user input
|
||||
2. **Use Regular Methods**: Prefer regular methods over direct operations
|
||||
3. **Server-side Only**: Keep sensitive operations server-side
|
||||
4. **Audit Logging**: Log important operations
|
||||
|
||||
### Performance
|
||||
|
||||
1. **Connection Pooling**: Use connection pooling for better performance
|
||||
2. **Batch Operations**: Use batch operations for bulk data
|
||||
3. **Caching**: Implement caching for frequently accessed data
|
||||
4. **Monitoring**: Monitor query performance and optimize as needed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Connection Failures**:
|
||||
- Check MONGO_URL configuration
|
||||
- Verify MongoDB server is running
|
||||
- Check network connectivity
|
||||
|
||||
2. **Query Errors**:
|
||||
- Verify query syntax
|
||||
- Check field names and types
|
||||
- Validate MongoDB version compatibility
|
||||
|
||||
3. **Performance Issues**:
|
||||
- Check indexes
|
||||
- Optimize queries
|
||||
- Monitor connection pool usage
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check MongoDB version
|
||||
mongo --eval "db.version()"
|
||||
|
||||
# Check connection status
|
||||
mongo --eval "db.runCommand({connectionStatus: 1})"
|
||||
|
||||
# Check indexes
|
||||
mongo --eval "db.collection.getIndexes()"
|
||||
|
||||
# Check query performance
|
||||
mongo --eval "db.collection.find().explain('executionStats')"
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Older MongoDB Versions
|
||||
|
||||
1. **Backup Data**: Always backup before migration
|
||||
2. **Test Compatibility**: Test with target MongoDB version
|
||||
3. **Update Drivers**: Use appropriate driver for MongoDB version
|
||||
4. **Run Migrations**: Execute any necessary data migrations
|
||||
5. **Validate**: Verify all functionality works correctly
|
||||
|
||||
### To Newer MongoDB Versions
|
||||
|
||||
1. **Check Compatibility**: Verify all queries are compatible
|
||||
2. **Update Queries**: Fix any deprecated method usage
|
||||
3. **Test Thoroughly**: Test all functionality
|
||||
4. **Monitor Performance**: Watch for performance changes
|
||||
5. **Update Documentation**: Update any version-specific documentation
|
||||
|
||||
## Support
|
||||
|
||||
For MongoDB compatibility issues:
|
||||
|
||||
1. Check this guide for known issues and solutions
|
||||
2. Review MongoDB release notes for version-specific changes
|
||||
3. Test with the MongoDB driver system
|
||||
4. Use the monitoring tools to diagnose issues
|
||||
5. Consult the Wekan community for additional help
|
||||
|
||||
## License
|
||||
|
||||
This MongoDB Compatibility Guide is part of Wekan and is licensed under the MIT License.
|
||||
@@ -0,0 +1,281 @@
|
||||
# MongoDB Driver System
|
||||
|
||||
## Overview
|
||||
|
||||
The MongoDB Driver System provides automatic MongoDB version detection and driver selection to support MongoDB versions 3.0 through 8.0 with compatible Node.js drivers. This system eliminates the need for manual driver configuration and provides seamless compatibility across all supported MongoDB versions.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic Version Detection**: Detects MongoDB server version from wire protocol errors
|
||||
- **Dynamic Driver Selection**: Automatically selects the appropriate Node.js driver based on detected version
|
||||
- **Fallback Mechanism**: Tries multiple drivers if the first attempt fails
|
||||
- **Connection Retry Logic**: Retries connections with different drivers on failure
|
||||
- **Real-time Monitoring**: Provides connection statistics and monitoring capabilities
|
||||
- **Meteor Integration**: Seamlessly integrates with Meteor's MongoDB connection system
|
||||
|
||||
## Supported MongoDB Versions
|
||||
|
||||
| MongoDB Version | Node.js Driver | Driver Version | Package Name |
|
||||
|----------------|----------------|----------------|--------------|
|
||||
| 3.0 - 3.6 | mongodb@3.7.4 | 3.7.4 | mongodb3legacy |
|
||||
| 4.0 - 4.4 | mongodb@4.17.2 | 4.17.2 | mongodb4legacy |
|
||||
| 5.0 | mongodb@5.9.2 | 5.9.2 | mongodb5legacy |
|
||||
| 6.0 | mongodb@6.3.0 | 6.3.0 | mongodb6legacy |
|
||||
| 7.0 | mongodb@7.0.1 | 7.0.1 | mongodb7legacy |
|
||||
| 8.0 | mongodb@6.9.0 | 6.9.0 | mongodb8legacy |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **MongoDBDriverManager** (`models/lib/mongodbDriverManager.js`)
|
||||
- Manages driver compatibility matrix
|
||||
- Detects MongoDB version from wire protocol errors
|
||||
- Selects appropriate driver based on detected version
|
||||
- Tracks connection attempts and statistics
|
||||
|
||||
2. **MongoDBConnectionManager** (`models/lib/mongodbConnectionManager.js`)
|
||||
- Handles MongoDB connections with automatic driver selection
|
||||
- Implements connection retry logic with different drivers
|
||||
- Manages connection pooling and lifecycle
|
||||
- Provides fallback mechanism for unsupported versions
|
||||
|
||||
3. **MeteorMongoIntegration** (`models/lib/meteorMongoIntegration.js`)
|
||||
- Integrates with Meteor's MongoDB connection system
|
||||
- Overrides Meteor's connection methods to use custom drivers
|
||||
- Provides Meteor-compatible connection objects
|
||||
- Enhances collections with additional methods
|
||||
|
||||
4. **MongoDB Driver Startup** (`server/mongodb-driver-startup.js`)
|
||||
- Initializes the system on server startup
|
||||
- Provides server-side methods for monitoring and debugging
|
||||
- Sets up real-time monitoring publications
|
||||
|
||||
## Installation
|
||||
|
||||
The MongoDB driver system is automatically installed when you install Wekan dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
All required driver packages are included in `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"mongodb3legacy": "npm:mongodb@3.7.4",
|
||||
"mongodb4legacy": "npm:mongodb@4.17.2",
|
||||
"mongodb5legacy": "npm:mongodb@5.9.2",
|
||||
"mongodb6legacy": "npm:mongodb@6.3.0",
|
||||
"mongodb7legacy": "npm:mongodb@7.0.1",
|
||||
"mongodb8legacy": "npm:mongodb@6.9.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Automatic Operation
|
||||
|
||||
The system works automatically without any configuration required:
|
||||
|
||||
1. **Startup**: The system initializes automatically when Wekan starts
|
||||
2. **Connection**: When connecting to MongoDB, the system detects the server version
|
||||
3. **Driver Selection**: The appropriate driver is selected based on the detected version
|
||||
4. **Fallback**: If the first driver fails, fallback drivers are tried automatically
|
||||
|
||||
### Manual Testing
|
||||
|
||||
You can test the system using the provided test script:
|
||||
|
||||
```bash
|
||||
node test-mongodb-drivers.js
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
The system provides several ways to monitor its operation:
|
||||
|
||||
#### Server-side Methods
|
||||
|
||||
```javascript
|
||||
// Get connection statistics
|
||||
Meteor.call('mongodb-driver-stats', (error, result) => {
|
||||
console.log('Connection Stats:', result);
|
||||
});
|
||||
|
||||
// Test connection
|
||||
Meteor.call('mongodb-driver-test-connection', (error, result) => {
|
||||
console.log('Connection Test:', result);
|
||||
});
|
||||
|
||||
// Get supported versions
|
||||
Meteor.call('mongodb-driver-supported-versions', (error, result) => {
|
||||
console.log('Supported Versions:', result);
|
||||
});
|
||||
|
||||
// Reset system
|
||||
Meteor.call('mongodb-driver-reset', (error, result) => {
|
||||
console.log('Reset Result:', result);
|
||||
});
|
||||
```
|
||||
|
||||
#### Real-time Monitoring
|
||||
|
||||
Subscribe to the monitoring publication:
|
||||
|
||||
```javascript
|
||||
Meteor.subscribe('mongodb-driver-monitor');
|
||||
```
|
||||
|
||||
Access the data in your template:
|
||||
|
||||
```javascript
|
||||
Template.yourTemplate.helpers({
|
||||
driverStats() {
|
||||
return MongoDBDriverMonitor.findOne('stats');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Wire Protocol Error Detection
|
||||
|
||||
The system detects MongoDB versions by analyzing wire protocol errors. Here are the error patterns used:
|
||||
|
||||
### MongoDB 3.x
|
||||
- `unsupported wire protocol version`
|
||||
- `wire protocol version 0/1/2/3`
|
||||
- `protocol version 0/1/2/3`
|
||||
|
||||
### MongoDB 4.x
|
||||
- `wire protocol version 4/5/6`
|
||||
- `protocol version 4/5/6`
|
||||
|
||||
### MongoDB 5.x
|
||||
- `wire protocol version 7`
|
||||
- `protocol version 7`
|
||||
|
||||
### MongoDB 6.x
|
||||
- `wire protocol version 8`
|
||||
- `protocol version 8`
|
||||
|
||||
### MongoDB 7.x
|
||||
- `wire protocol version 9`
|
||||
- `protocol version 9`
|
||||
|
||||
### MongoDB 8.x
|
||||
- `wire protocol version 10`
|
||||
- `protocol version 10`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The system uses the standard MongoDB environment variables:
|
||||
|
||||
- `MONGO_URL`: MongoDB connection string
|
||||
- `MONGO_OPLOG_URL`: MongoDB oplog connection string (optional)
|
||||
|
||||
### Connection Options
|
||||
|
||||
You can customize connection options by modifying the default options in `mongodbConnectionManager.js`:
|
||||
|
||||
```javascript
|
||||
const defaultOptions = {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
// Add your custom options here
|
||||
};
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Driver Not Found**
|
||||
- Ensure all driver packages are installed: `npm install`
|
||||
- Check that package.json contains all required drivers
|
||||
|
||||
2. **Connection Failures**
|
||||
- Verify MONGO_URL is correctly set
|
||||
- Check MongoDB server is running and accessible
|
||||
- Review connection logs for specific error messages
|
||||
|
||||
3. **Version Detection Issues**
|
||||
- Check if MongoDB server is responding
|
||||
- Verify wire protocol error patterns match your MongoDB version
|
||||
- Review connection attempt logs
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable debug logging by setting the DEBUG environment variable:
|
||||
|
||||
```bash
|
||||
DEBUG=mongodb-driver* node main.js
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
The system provides detailed logging:
|
||||
|
||||
```
|
||||
=== MongoDB Driver System Startup ===
|
||||
MONGO_URL found, initializing MongoDB driver system...
|
||||
Connection string: mongodb://***:***@localhost:27017/wekan
|
||||
Initializing Meteor MongoDB Integration...
|
||||
Testing MongoDB connection...
|
||||
✅ MongoDB connection test successful
|
||||
Driver: mongodb6legacy
|
||||
Version: 6.0
|
||||
MongoDB Driver System Statistics:
|
||||
Initialized: true
|
||||
Custom Connection: true
|
||||
Supported Versions: 3.0, 3.2, 3.4, 3.6, 4.0, 4.2, 4.4, 5.0, 6.0, 7.0, 8.0
|
||||
=== MongoDB Driver System Ready ===
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Connection Pooling**: Each driver maintains its own connection pool
|
||||
- **Driver Selection**: Version detection happens only on first connection
|
||||
- **Fallback Overhead**: Fallback attempts add minimal overhead
|
||||
- **Memory Usage**: Multiple drivers are loaded but only one is active
|
||||
|
||||
## Security
|
||||
|
||||
- **Credential Protection**: Connection strings are logged with credentials masked
|
||||
- **Access Control**: Monitoring methods require user authentication
|
||||
- **Error Handling**: Sensitive information is not exposed in error messages
|
||||
|
||||
## Migration from Standard MongoDB
|
||||
|
||||
If you're migrating from standard MongoDB connections:
|
||||
|
||||
1. **No Code Changes Required**: The system is backward compatible
|
||||
2. **Automatic Detection**: Version detection happens automatically
|
||||
3. **Gradual Migration**: You can test with specific drivers before full migration
|
||||
4. **Rollback**: You can disable the system and return to standard connections
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Additional MongoDB Versions**: Support for newer MongoDB versions as they're released
|
||||
- **Performance Optimization**: Connection pooling optimizations
|
||||
- **Enhanced Monitoring**: More detailed metrics and alerting
|
||||
- **Configuration UI**: Web interface for driver configuration
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
|
||||
1. Check the troubleshooting section above
|
||||
2. Review the logs for specific error messages
|
||||
3. Test with the provided test script
|
||||
4. Use the monitoring methods to gather diagnostic information
|
||||
|
||||
## License
|
||||
|
||||
This MongoDB Driver System is part of Wekan and is licensed under the MIT License.
|
||||
@@ -0,0 +1,189 @@
|
||||
# MongoDB Version Management System
|
||||
|
||||
This document describes the MongoDB version management system that automatically detects MongoDB server versions and switches to the appropriate binary and Node.js driver.
|
||||
|
||||
## Overview
|
||||
|
||||
The system provides:
|
||||
- **Automatic version detection** based on connection attempts and wire protocol errors
|
||||
- **Automatic binary switching** to the correct MongoDB server version
|
||||
- **Automatic driver selection** using the appropriate Node.js MongoDB driver
|
||||
- **Support for MongoDB versions 3-8** with fallback mechanisms
|
||||
|
||||
## Current Status
|
||||
|
||||
### Available MongoDB Server Binaries
|
||||
- ✅ **MongoDB 3.2.22** - Available at `/snap/wekan/current/migratemongo/bin/`
|
||||
- ❌ **MongoDB 4.4.28** - Not available (needs to be added to snap package)
|
||||
- ❌ **MongoDB 5.0.28** - Not available (needs to be added to snap package)
|
||||
- ❌ **MongoDB 6.0.15** - Not available (needs to be added to snap package)
|
||||
- ✅ **MongoDB 7.0.25** - Available at `/snap/wekan/current/bin/`
|
||||
- ❌ **MongoDB 8.0.4** - Not available (needs to be added to snap package)
|
||||
|
||||
### Available MongoDB Node.js Drivers
|
||||
- ✅ **MongoDB 3.7.4** - Available as `mongodb3legacy`
|
||||
- ✅ **MongoDB 4.17.2** - Available as `mongodb4legacy`
|
||||
- ✅ **MongoDB 5.9.2** - Available as `mongodb5legacy`
|
||||
- ✅ **MongoDB 6.3.0** - Available as `mongodb6legacy`
|
||||
- ✅ **MongoDB 7.0.1** - Available as `mongodb7legacy`
|
||||
- ✅ **MongoDB 8.0** - Available as `mongodb8legacy`
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Version Detection
|
||||
The system attempts to connect to MongoDB using the latest available binary (MongoDB 7.x) and analyzes the response:
|
||||
|
||||
- **Success**: If connection succeeds, it's MongoDB 7.x
|
||||
- **Wire Protocol Error**: If connection fails with wire protocol errors, it detects the version based on the protocol version number:
|
||||
- Protocol 0-3: MongoDB 3.x
|
||||
- Protocol 4: MongoDB 4.x
|
||||
- Protocol 5: MongoDB 5.x
|
||||
- Protocol 6: MongoDB 6.x
|
||||
- Protocol 7: MongoDB 7.x
|
||||
- Protocol 8: MongoDB 8.x
|
||||
|
||||
### 2. Binary Switching
|
||||
Based on the detected version, the system switches to the appropriate MongoDB server binary:
|
||||
|
||||
```bash
|
||||
# MongoDB 3.x
|
||||
export PATH="/snap/wekan/current/migratemongo/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/snap/wekan/current/migratemongo/lib:$LD_LIBRARY_PATH"
|
||||
|
||||
# MongoDB 7.x (default)
|
||||
export PATH="/snap/wekan/current/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/snap/wekan/current/usr/lib:$LD_LIBRARY_PATH"
|
||||
```
|
||||
|
||||
### 3. Driver Selection
|
||||
The Node.js application automatically selects the appropriate MongoDB driver based on the detected version using the `mongodbDriverManager` system.
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Removed Migration Files
|
||||
- ❌ `snap-src/bin/mongodb-migrate` - Removed
|
||||
- ❌ `snap-src/bin/mongodb-migration-web` - Removed
|
||||
- ❌ `snap-src/bin/mongodb-migration-status` - Removed
|
||||
|
||||
### Updated Files
|
||||
- ✅ `snap-src/bin/mongodb-control` - Completely rewritten with version detection
|
||||
- ✅ `snap-src/bin/mongodb-version-manager` - New utility for managing versions
|
||||
|
||||
### Node.js Driver System
|
||||
- ✅ `models/lib/mongodbDriverManager.js` - Manages multiple MongoDB drivers
|
||||
- ✅ `models/lib/mongodbConnectionManager.js` - Handles connections with version detection
|
||||
- ✅ `models/lib/meteorMongoIntegration.js` - Integrates with Meteor's Mongo.Collection
|
||||
- ✅ `server/mongodb-driver-startup.js` - Initializes the driver system
|
||||
|
||||
## Adding Missing MongoDB Server Binaries
|
||||
|
||||
To add MongoDB versions 4, 5, 6, and 8 to the snap package, you need to:
|
||||
|
||||
### 1. Download MongoDB Server Binaries
|
||||
Download the appropriate MongoDB server binaries for each version:
|
||||
|
||||
```bash
|
||||
# MongoDB 4.4.28
|
||||
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.4.28.tgz
|
||||
|
||||
# MongoDB 5.0.28
|
||||
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-5.0.28.tgz
|
||||
|
||||
# MongoDB 6.0.15
|
||||
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-6.0.15.tgz
|
||||
|
||||
# MongoDB 8.0.4
|
||||
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-8.0.4.tgz
|
||||
```
|
||||
|
||||
### 2. Extract and Install in Snap Package
|
||||
Extract each version to the appropriate directory in the snap package:
|
||||
|
||||
```bash
|
||||
# Extract MongoDB 4.x
|
||||
tar -xzf mongodb-linux-x86_64-4.4.28.tgz
|
||||
mkdir -p /snap/wekan/current/mongodb4/bin
|
||||
cp mongodb-linux-x86_64-4.4.28/bin/* /snap/wekan/current/mongodb4/bin/
|
||||
|
||||
# Extract MongoDB 5.x
|
||||
tar -xzf mongodb-linux-x86_64-5.0.28.tgz
|
||||
mkdir -p /snap/wekan/current/mongodb5/bin
|
||||
cp mongodb-linux-x86_64-5.0.28/bin/* /snap/wekan/current/mongodb5/bin/
|
||||
|
||||
# Extract MongoDB 6.x
|
||||
tar -xzf mongodb-linux-x86_64-6.0.15.tgz
|
||||
mkdir -p /snap/wekan/current/mongodb6/bin
|
||||
cp mongodb-linux-x86_64-6.0.15/bin/* /snap/wekan/current/mongodb6/bin/
|
||||
|
||||
# Extract MongoDB 8.x
|
||||
tar -xzf mongodb-linux-x86_64-8.0.4.tgz
|
||||
mkdir -p /snap/wekan/current/mongodb8/bin
|
||||
cp mongodb-linux-x86_64-8.0.4/bin/* /snap/wekan/current/mongodb8/bin/
|
||||
```
|
||||
|
||||
### 3. Update Snap Package Build
|
||||
Modify the snap package build process to include these binaries in the final package.
|
||||
|
||||
## Usage
|
||||
|
||||
### Check Available Versions
|
||||
```bash
|
||||
$SNAP/bin/mongodb-version-manager versions
|
||||
```
|
||||
|
||||
### Check Current Active Version
|
||||
```bash
|
||||
$SNAP/bin/mongodb-version-manager active
|
||||
```
|
||||
|
||||
### Force Version Detection
|
||||
```bash
|
||||
$SNAP/bin/mongodb-version-manager detect
|
||||
```
|
||||
|
||||
### View Detection Log
|
||||
```bash
|
||||
$SNAP/bin/mongodb-version-manager log
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
- `MONGO_URL` - MongoDB connection URL (default: `mongodb://127.0.0.1:27017/wekan`)
|
||||
- `SNAP_COMMON` - Snap common directory for logs and version files
|
||||
|
||||
### Version Files
|
||||
- `${SNAP_COMMON}/mongodb-active-version` - Caches the currently active version
|
||||
- `${SNAP_COMMON}/mongodb-version-detection.log` - Logs version detection events
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **No Downtime**: Automatic version detection and switching without manual intervention
|
||||
2. **Backward Compatibility**: Works with existing MongoDB 3.x databases
|
||||
3. **Forward Compatibility**: Ready for MongoDB 8.x when available
|
||||
4. **Automatic Fallback**: Falls back to MongoDB 7.x if detection fails
|
||||
5. **Comprehensive Logging**: Detailed logs for troubleshooting
|
||||
6. **No Migrations**: Eliminates the need for database migrations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Version Detection Not Working
|
||||
1. Check the detection log: `$SNAP/bin/mongodb-version-manager log`
|
||||
2. Force re-detection: `$SNAP/bin/mongodb-version-manager detect`
|
||||
3. Restart MongoDB service
|
||||
|
||||
### Wrong Version Selected
|
||||
1. Clear the cached version: `rm ${SNAP_COMMON}/mongodb-active-version`
|
||||
2. Restart MongoDB service
|
||||
|
||||
### Connection Issues
|
||||
1. Verify MongoDB server is running
|
||||
2. Check MONGO_URL environment variable
|
||||
3. Review MongoDB server logs
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Add Missing Binaries**: Include MongoDB 4, 5, 6, and 8 server binaries in snap package
|
||||
2. **Health Checks**: Add health monitoring for different MongoDB versions
|
||||
3. **Performance Metrics**: Track performance across different versions
|
||||
4. **Auto-Upgrade**: Automatically upgrade to newer versions when available
|
||||
@@ -0,0 +1,306 @@
|
||||
# Enhanced Attachment Migration System
|
||||
|
||||
## Overview
|
||||
|
||||
The Enhanced Attachment Migration System provides a comprehensive solution for managing attachment storage across multiple backends (filesystem, MongoDB GridFS, S3/MinIO) with CPU throttling, real-time monitoring, and secure configuration management.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Multi-Backend Storage Support
|
||||
- **Filesystem Storage**: Local file system storage using `WRITABLE_PATH`
|
||||
- **MongoDB GridFS**: Database-based file storage
|
||||
- **S3/MinIO**: Cloud and object storage compatibility
|
||||
|
||||
### 2. CPU Throttling
|
||||
- **Automatic CPU Monitoring**: Real-time CPU usage tracking
|
||||
- **Configurable Thresholds**: Set CPU usage limits (10-90%)
|
||||
- **Automatic Pausing**: Migration pauses when CPU threshold is exceeded
|
||||
- **Resume Capability**: Continue migration when CPU usage drops
|
||||
|
||||
### 3. Batch Processing
|
||||
- **Configurable Batch Size**: Process 1-100 attachments per batch
|
||||
- **Adjustable Delays**: Set delays between batches (100-10000ms)
|
||||
- **Progress Tracking**: Real-time progress monitoring
|
||||
- **Queue Management**: Intelligent migration queue handling
|
||||
|
||||
### 4. Security Features
|
||||
- **Password Protection**: S3 secret keys are never displayed
|
||||
- **Admin-Only Access**: All operations require admin privileges
|
||||
- **Secure Configuration**: Environment-based configuration management
|
||||
- **Audit Logging**: Comprehensive migration logging
|
||||
|
||||
### 5. Real-Time Monitoring
|
||||
- **Storage Statistics**: Live attachment counts and sizes
|
||||
- **Visual Charts**: Storage distribution visualization
|
||||
- **Migration Status**: Real-time migration progress
|
||||
- **System Metrics**: CPU, memory, and performance monitoring
|
||||
|
||||
## Admin Panel Interface
|
||||
|
||||
### Storage Settings
|
||||
- **Filesystem Configuration**: View and configure filesystem paths
|
||||
- **GridFS Status**: Monitor MongoDB GridFS availability
|
||||
- **S3/MinIO Configuration**: Secure S3/MinIO setup and testing
|
||||
- **Connection Testing**: Validate storage backend connections
|
||||
|
||||
### Migration Controls
|
||||
- **Batch Configuration**: Set batch size, delays, and CPU thresholds
|
||||
- **Migration Actions**: Start, pause, resume, and stop migrations
|
||||
- **Progress Monitoring**: Real-time progress bars and statistics
|
||||
- **Log Viewing**: Live migration logs with timestamps
|
||||
|
||||
### Monitoring Dashboard
|
||||
- **Storage Distribution**: Visual breakdown of attachment storage
|
||||
- **Size Analytics**: Total and per-storage size statistics
|
||||
- **Performance Metrics**: System resource usage
|
||||
- **Export Capabilities**: Download monitoring data
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### Filesystem Storage
|
||||
```bash
|
||||
# Base writable path for all file storage
|
||||
WRITABLE_PATH=/data
|
||||
|
||||
# Attachments will be stored at: ${WRITABLE_PATH}/attachments
|
||||
# Avatars will be stored at: ${WRITABLE_PATH}/avatars
|
||||
```
|
||||
|
||||
#### S3/MinIO Storage
|
||||
```bash
|
||||
# S3 configuration (JSON format)
|
||||
S3='{"s3":{"key":"access-key","secret":"secret-key","bucket":"bucket-name","endPoint":"s3.amazonaws.com","port":443,"sslEnabled":true,"region":"us-east-1"}}'
|
||||
|
||||
# Alternative: S3 secret file (Docker secrets)
|
||||
S3_SECRET_FILE=/run/secrets/s3_secret
|
||||
```
|
||||
|
||||
### Migration Settings
|
||||
|
||||
#### Default Configuration
|
||||
- **Batch Size**: 10 attachments per batch
|
||||
- **Delay**: 1000ms between batches
|
||||
- **CPU Threshold**: 70% maximum CPU usage
|
||||
- **Auto-pause**: When CPU exceeds threshold
|
||||
|
||||
#### Customization
|
||||
All settings can be adjusted through the admin panel:
|
||||
- Batch size: 1-100 attachments
|
||||
- Delay: 100-10000ms
|
||||
- CPU threshold: 10-90%
|
||||
|
||||
## Usage
|
||||
|
||||
### Starting a Migration
|
||||
|
||||
1. **Access Admin Panel**: Navigate to Settings → Attachment Settings
|
||||
2. **Configure Migration**: Set batch size, delay, and CPU threshold
|
||||
3. **Select Target Storage**: Choose filesystem, GridFS, or S3
|
||||
4. **Start Migration**: Click the appropriate migration button
|
||||
5. **Monitor Progress**: Watch real-time progress and logs
|
||||
|
||||
### Migration Process
|
||||
|
||||
1. **Queue Initialization**: All attachments are queued for migration
|
||||
2. **Batch Processing**: Attachments are processed in configurable batches
|
||||
3. **CPU Monitoring**: System continuously monitors CPU usage
|
||||
4. **Automatic Pausing**: Migration pauses if CPU threshold is exceeded
|
||||
5. **Resume Capability**: Migration resumes when CPU usage drops
|
||||
6. **Progress Tracking**: Real-time progress updates and logging
|
||||
|
||||
### Monitoring Migration
|
||||
|
||||
- **Progress Bar**: Visual progress indicator
|
||||
- **Statistics**: Total, migrated, and remaining attachment counts
|
||||
- **Status Display**: Current migration status (running, paused, idle)
|
||||
- **Log View**: Real-time migration logs with timestamps
|
||||
- **Control Buttons**: Pause, resume, and stop migration
|
||||
|
||||
## API Reference
|
||||
|
||||
### Server Methods
|
||||
|
||||
#### Migration Control
|
||||
```javascript
|
||||
// Start migration
|
||||
Meteor.call('startAttachmentMigration', {
|
||||
targetStorage: 'filesystem', // 'filesystem', 'gridfs', 's3'
|
||||
batchSize: 10,
|
||||
delayMs: 1000,
|
||||
cpuThreshold: 70
|
||||
});
|
||||
|
||||
// Pause migration
|
||||
Meteor.call('pauseAttachmentMigration');
|
||||
|
||||
// Resume migration
|
||||
Meteor.call('resumeAttachmentMigration');
|
||||
|
||||
// Stop migration
|
||||
Meteor.call('stopAttachmentMigration');
|
||||
```
|
||||
|
||||
#### Configuration Management
|
||||
```javascript
|
||||
// Get storage configuration
|
||||
Meteor.call('getAttachmentStorageConfiguration');
|
||||
|
||||
// Test S3 connection
|
||||
Meteor.call('testS3Connection', { secretKey: 'new-secret-key' });
|
||||
|
||||
// Save S3 settings
|
||||
Meteor.call('saveS3Settings', { secretKey: 'new-secret-key' });
|
||||
```
|
||||
|
||||
#### Monitoring
|
||||
```javascript
|
||||
// Get monitoring data
|
||||
Meteor.call('getAttachmentMonitoringData');
|
||||
|
||||
// Refresh monitoring data
|
||||
Meteor.call('refreshAttachmentMonitoringData');
|
||||
|
||||
// Export monitoring data
|
||||
Meteor.call('exportAttachmentMonitoringData');
|
||||
```
|
||||
|
||||
### Publications
|
||||
|
||||
#### Real-Time Updates
|
||||
```javascript
|
||||
// Subscribe to migration status
|
||||
Meteor.subscribe('attachmentMigrationStatus');
|
||||
|
||||
// Subscribe to monitoring data
|
||||
Meteor.subscribe('attachmentMonitoringData');
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### CPU Throttling
|
||||
- **Automatic Monitoring**: CPU usage is checked every 5 seconds
|
||||
- **Threshold-Based Pausing**: Migration pauses when CPU exceeds threshold
|
||||
- **Resume Logic**: Migration resumes when CPU usage drops below threshold
|
||||
- **Configurable Limits**: CPU thresholds can be adjusted (10-90%)
|
||||
|
||||
### Batch Processing
|
||||
- **Configurable Batches**: Batch size can be adjusted (1-100)
|
||||
- **Delay Control**: Delays between batches prevent system overload
|
||||
- **Queue Management**: Intelligent queue processing with error handling
|
||||
- **Progress Tracking**: Real-time progress updates
|
||||
|
||||
### Memory Management
|
||||
- **Streaming Processing**: Large files are processed using streams
|
||||
- **Memory Monitoring**: System memory usage is tracked
|
||||
- **Garbage Collection**: Automatic cleanup of processed data
|
||||
- **Error Recovery**: Robust error handling and recovery
|
||||
|
||||
## Security
|
||||
|
||||
### Access Control
|
||||
- **Admin-Only Access**: All operations require admin privileges
|
||||
- **User Authentication**: Proper user authentication and authorization
|
||||
- **Session Management**: Secure session handling
|
||||
|
||||
### Data Protection
|
||||
- **Password Security**: S3 secret keys are never displayed in UI
|
||||
- **Environment Variables**: Sensitive data stored in environment variables
|
||||
- **Secure Transmission**: All data transmission is encrypted
|
||||
- **Audit Logging**: Comprehensive logging of all operations
|
||||
|
||||
### Configuration Security
|
||||
- **Read-Only Display**: Sensitive configuration is displayed as read-only
|
||||
- **Password Updates**: Only new passwords can be set, not viewed
|
||||
- **Connection Testing**: Secure connection testing without exposing credentials
|
||||
- **Environment Isolation**: Configuration isolated from application code
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Migration Not Starting
|
||||
- **Check Permissions**: Ensure user has admin privileges
|
||||
- **Verify Configuration**: Check storage backend configuration
|
||||
- **Review Logs**: Check server logs for error messages
|
||||
- **Test Connections**: Verify storage backend connectivity
|
||||
|
||||
#### High CPU Usage
|
||||
- **Reduce Batch Size**: Decrease batch size to reduce CPU load
|
||||
- **Increase Delays**: Add longer delays between batches
|
||||
- **Lower CPU Threshold**: Reduce CPU threshold for earlier pausing
|
||||
- **Monitor System**: Check system resource usage
|
||||
|
||||
#### Migration Pausing Frequently
|
||||
- **Check CPU Threshold**: Verify CPU threshold settings
|
||||
- **Monitor System Load**: Check for other high-CPU processes
|
||||
- **Adjust Settings**: Increase CPU threshold or reduce batch size
|
||||
- **System Optimization**: Optimize system performance
|
||||
|
||||
#### Storage Connection Issues
|
||||
- **Verify Credentials**: Check S3/MinIO credentials
|
||||
- **Test Connectivity**: Use connection test feature
|
||||
- **Check Network**: Verify network connectivity
|
||||
- **Review Configuration**: Validate storage configuration
|
||||
|
||||
### Debug Information
|
||||
|
||||
#### Migration Logs
|
||||
- **Real-Time Logs**: View live migration logs in admin panel
|
||||
- **Server Logs**: Check server console for detailed logs
|
||||
- **Error Messages**: Review error messages for specific issues
|
||||
- **Progress Tracking**: Monitor migration progress and statistics
|
||||
|
||||
#### System Monitoring
|
||||
- **CPU Usage**: Monitor CPU usage during migration
|
||||
- **Memory Usage**: Track memory consumption
|
||||
- **Disk I/O**: Monitor disk input/output operations
|
||||
- **Network Usage**: Check network bandwidth usage
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Migration Planning
|
||||
- **Schedule During Low Usage**: Run migrations during low-traffic periods
|
||||
- **Test First**: Test migration with small batches first
|
||||
- **Monitor Resources**: Keep an eye on system resources
|
||||
- **Have Backup**: Ensure data backup before migration
|
||||
|
||||
### Performance Optimization
|
||||
- **Optimize Batch Size**: Find optimal batch size for your system
|
||||
- **Adjust Delays**: Set appropriate delays between batches
|
||||
- **Monitor CPU**: Set realistic CPU thresholds
|
||||
- **Use Monitoring**: Regularly check monitoring data
|
||||
|
||||
### Security Practices
|
||||
- **Regular Updates**: Keep S3 credentials updated
|
||||
- **Access Control**: Limit admin access to necessary users
|
||||
- **Audit Logs**: Regularly review migration logs
|
||||
- **Environment Security**: Secure environment variable storage
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
- **Incremental Migration**: Migrate only changed attachments
|
||||
- **Parallel Processing**: Support for parallel migration streams
|
||||
- **Advanced Scheduling**: Time-based migration scheduling
|
||||
- **Compression Support**: Built-in file compression during migration
|
||||
|
||||
### Integration Opportunities
|
||||
- **Cloud Storage**: Additional cloud storage providers
|
||||
- **CDN Integration**: Content delivery network support
|
||||
- **Backup Integration**: Automated backup during migration
|
||||
- **Analytics**: Advanced storage analytics and reporting
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
1. Check this documentation
|
||||
2. Review server logs
|
||||
3. Use the monitoring tools
|
||||
4. Consult the Wekan community
|
||||
5. Report issues with detailed information
|
||||
|
||||
## License
|
||||
|
||||
This Enhanced Attachment Migration System is part of Wekan and is licensed under the MIT License.
|
||||
File renamed without changes.
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
|
||||
|
||||
Right click and download files 1-4:
|
||||
|
||||
1. [wekan-7.99-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v7.99/wekan-7.99-amd64-windows.zip)
|
||||
1. [wekan-8.01-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64-windows.zip)
|
||||
|
||||
2. [node.exe](https://nodejs.org/dist/latest-v14.x/win-x64/node.exe)
|
||||
|
||||
@@ -22,7 +22,7 @@ Right click and download files 1-4:
|
||||
|
||||
6. Double click `mongodb-windows-x86_64-7.0.25-signed.msi` . In installer, uncheck downloading MongoDB compass.
|
||||
|
||||
7. Unzip `wekan-7.99-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
7. Unzip `wekan-8.01-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
|
||||
```
|
||||
bundle (directory)
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "رفع صورة شخصية",
|
||||
"uploaded-avatar": "تم رفع الصورة الشخصية",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Качване",
|
||||
"upload-avatar": "Качване на аватар",
|
||||
"uploaded-avatar": "Качихте аватар",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Puja",
|
||||
"upload-avatar": "Actualitza avatar",
|
||||
"uploaded-avatar": "Avatar actualitzat",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "URL personalitzat de la imatge del logotip de la cantonada superior esquerra",
|
||||
"custom-top-left-corner-logo-link-url": "URL de l'enllaç del logotip de la cantonada superior esquerra personalitzada",
|
||||
"custom-top-left-corner-logo-height": "Alçada personalitzada del logotip de la cantonada superior esquerra. Per defecte: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Nahrát",
|
||||
"upload-avatar": "Nahrát avatar",
|
||||
"uploaded-avatar": "Avatar nahrán",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "URL obrázku vlastního loga v levém horním rohu",
|
||||
"custom-top-left-corner-logo-link-url": "URL odkazu vlastního loga v levém horním rohu",
|
||||
"custom-top-left-corner-logo-height": "Výška vlastního loga v horním levém rohu. Defaultně: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Nahrát",
|
||||
"upload-avatar": "Nahrát avatar",
|
||||
"uploaded-avatar": "Avatar nahrán",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "URL obrázku vlastního loga v levém horním rohu",
|
||||
"custom-top-left-corner-logo-link-url": "URL odkazu vlastního loga v levém horním rohu",
|
||||
"custom-top-left-corner-logo-height": "Výška vlastního loga v horním levém rohu. Defaultně: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Overfør",
|
||||
"upload-avatar": "Overfør en avatar",
|
||||
"uploaded-avatar": "Overførte en avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Profilbild hochladen",
|
||||
"uploaded-avatar": "Profilbild hochgeladen",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Benutzerdefiniertes Logo oben links Bild URL",
|
||||
"custom-top-left-corner-logo-link-url": "Benutzerdefiniertes Logo oben links Link URL",
|
||||
"custom-top-left-corner-logo-height": "Benutzerdefiniertes Logo oben links Höhe. Voreinstellung: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Profilbild hochladen",
|
||||
"uploaded-avatar": "Profilbild hochgeladen",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Benutzerdefiniertes Logo oben links Bild URL",
|
||||
"custom-top-left-corner-logo-link-url": "Benutzerdefiniertes Logo oben links Link URL",
|
||||
"custom-top-left-corner-logo-height": "Benutzerdefiniertes Logo oben links Höhe. Voreinstellung: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Profilbild hochladen",
|
||||
"uploaded-avatar": "Profilbild hochgeladen",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Benutzerdefiniertes Logo oben links Bild URL",
|
||||
"custom-top-left-corner-logo-link-url": "Benutzerdefiniertes Logo oben links Link URL",
|
||||
"custom-top-left-corner-logo-height": "Benutzerdefiniertes Logo oben links Höhe. Voreinstellung: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Profilbild hochladen",
|
||||
"uploaded-avatar": "Profilbild hochgeladen",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Benutzerdefiniertes Logo oben links Bild URL",
|
||||
"custom-top-left-corner-logo-link-url": "Benutzerdefiniertes Logo oben links Link URL",
|
||||
"custom-top-left-corner-logo-height": "Benutzerdefiniertes Logo oben links Höhe. Voreinstellung: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Μεταφόρτωση",
|
||||
"upload-avatar": "Μεταφόρτωση ενός avatar",
|
||||
"uploaded-avatar": "Μεταφόρτωσε ένα avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Μεταφόρτωση",
|
||||
"upload-avatar": "Μεταφόρτωση ενός avatar",
|
||||
"uploaded-avatar": "Μεταφόρτωσε ένα avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Alŝuti",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Cargar",
|
||||
"upload-avatar": "Cargar un avatar",
|
||||
"uploaded-avatar": "Cargado un avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Cargar",
|
||||
"upload-avatar": "Cargar un avatar",
|
||||
"uploaded-avatar": "Avatar cargado",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Cargar",
|
||||
"upload-avatar": "Cargar un avatar",
|
||||
"uploaded-avatar": "Avatar cargado",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "URL de imagen del logo personalizado de la esquina superior izquierda",
|
||||
"custom-top-left-corner-logo-link-url": "URL de enlace del logo personalizado de la esquina superior izquierda",
|
||||
"custom-top-left-corner-logo-height": "Altura del logo de la esquina superior izquierda. Por defecto: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Cargar",
|
||||
"upload-avatar": "Cargar un avatar",
|
||||
"uploaded-avatar": "Avatar cargado",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Personalizar la URL del logotipo en la esquina superior izquierda",
|
||||
"custom-top-left-corner-logo-link-url": "Personalizar el enlace del logotipo de la esquina superior izquierda",
|
||||
"custom-top-left-corner-logo-height": "Altura personalizada del logo de la esquina superior izquierda. Por defecto: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Laadige üles",
|
||||
"upload-avatar": "Avatari üleslaadimine",
|
||||
"uploaded-avatar": "Avatari üleslaadimine",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Kohandatud vasakus ülemises nurgas oleva logo pildi URL",
|
||||
"custom-top-left-corner-logo-link-url": "Kohandatud vasakus ülemises nurgas olev logo link URL",
|
||||
"custom-top-left-corner-logo-height": "Kohandatud vasakpoolse ülemise nurga logo kõrgus. Vaikimisi: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Igo",
|
||||
"upload-avatar": "Igo avatar bat",
|
||||
"uploaded-avatar": "Avatar bat igo da",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Pertsonalizatutako goiko ezkerreko izkinako logotipoaren irudiaren URLa",
|
||||
"custom-top-left-corner-logo-link-url": "Pertsonalizatutako goiko ezkerreko izkinako logotipoaren estekaren URLa",
|
||||
"custom-top-left-corner-logo-height": "Pertsonalizatutako goiko ezkerreko izkinako logotipoaren zabalera. Lehenetsia: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "آپلود",
|
||||
"upload-avatar": "ارسال آواتار",
|
||||
"uploaded-avatar": "آواتار آپلود شد",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "آدرس تصویر لوگوی سفارشی در گوشه چپ و بالا",
|
||||
"custom-top-left-corner-logo-link-url": "آدرس لینک لوگوی سفارشی در گوشه چپ و بالا",
|
||||
"custom-top-left-corner-logo-height": "ارتفاع لوگوی سفارشی در گوشه چپ و بالا. پیشفرض: ۲۷",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "آپلود",
|
||||
"upload-avatar": "ارسال آواتار",
|
||||
"uploaded-avatar": "آواتار آپلود شد",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "آدرس تصویر لوگوی سفارشی در گوشه چپ و بالا",
|
||||
"custom-top-left-corner-logo-link-url": "آدرس لینک لوگوی سفارشی در گوشه چپ و بالا",
|
||||
"custom-top-left-corner-logo-height": "ارتفاع لوگوی سفارشی در گوشه چپ و بالا. پیشفرض: ۲۷",
|
||||
|
||||
@@ -177,14 +177,14 @@
|
||||
"boardChangeViewPopup-title": "Taulunäkymä",
|
||||
"boards": "Taulut",
|
||||
"board-view": "Taulunäkymä",
|
||||
"desktop-mode": "Desktop Mode",
|
||||
"mobile-mode": "Mobile Mode",
|
||||
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
|
||||
"zoom-in": "Zoom In",
|
||||
"zoom-out": "Zoom Out",
|
||||
"click-to-change-zoom": "Click to change zoom level",
|
||||
"zoom-level": "Zoom Level",
|
||||
"enter-zoom-level": "Enter zoom level (50-300%):",
|
||||
"desktop-mode": "Työpöytätila",
|
||||
"mobile-mode": "Mobiilitila",
|
||||
"mobile-desktop-toggle": "Vaihda mobiili- ja työpöytätilan välillä",
|
||||
"zoom-in": "Lähennä",
|
||||
"zoom-out": "Loitonna",
|
||||
"click-to-change-zoom": "Napsauta muuttaaksesi zoomaustasoa",
|
||||
"zoom-level": "Zoomaus taso",
|
||||
"enter-zoom-level": "Kirjoita zoomaus taso (50-300%):",
|
||||
"board-view-cal": "Kalenteri",
|
||||
"board-view-swimlanes": "Uimaradat",
|
||||
"board-view-collapse": "Pienennä",
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Lähetä",
|
||||
"upload-avatar": "Lähetä profiilikuva",
|
||||
"uploaded-avatar": "Profiilikuva lähetetty",
|
||||
"uploading-files": "Tiedostojen lisäys",
|
||||
"upload-failed": "Tiedostojen lisäys epäonnistui",
|
||||
"upload-completed": "Tiedostojen lisäys valmis",
|
||||
"custom-top-left-corner-logo-image-url": "Mukautettu oikean yläkulman logo kuvan URL",
|
||||
"custom-top-left-corner-logo-link-url": "Mukautettu oikean yläkulma logon linkki URL",
|
||||
"custom-top-left-corner-logo-height": "Mukautettu oikean yläkulman logokuvan korkeus. Oletus: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Télécharger",
|
||||
"upload-avatar": "Télécharger un avatar",
|
||||
"uploaded-avatar": "Avatar téléchargé",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "URL de l'Image du logo personnalisé dans le coin supérieur gauche",
|
||||
"custom-top-left-corner-logo-link-url": "Lien URL du logo personnalisé dans le coin supérieur gauche",
|
||||
"custom-top-left-corner-logo-height": "Hauteur du logo personnalisé dans le coin supérieur gauche. Défaut : 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Télécharger",
|
||||
"upload-avatar": "Télécharger un avatar",
|
||||
"uploaded-avatar": "Avatar téléchargé",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "URL de l'Image du logo personnalisé dans le coin supérieur gauche",
|
||||
"custom-top-left-corner-logo-link-url": "Lien URL du logo personnalisé dans le coin supérieur gauche",
|
||||
"custom-top-left-corner-logo-height": "Hauteur du logo personnalisé dans le coin supérieur gauche. Défaut : 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Enviar",
|
||||
"upload-avatar": "Enviar un avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Enviar",
|
||||
"upload-avatar": "Enviar un avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "העלאה",
|
||||
"upload-avatar": "העלאת תמונת משתמש",
|
||||
"uploaded-avatar": "הועלתה תמונה משתמש",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "כתובת תמונת לוגו משלך לפינה הימנית העליונה",
|
||||
"custom-top-left-corner-logo-link-url": "כתובת קישור לוגו משלך לפינה הימנית העליונה",
|
||||
"custom-top-left-corner-logo-height": "גובה לוגו מותאם אישית בפינה הימנית העליונה. בררת מחדל: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Feltöltés",
|
||||
"upload-avatar": "Egy avatár feltöltése",
|
||||
"uploaded-avatar": "Egy avatár feltöltve",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Egyedi Bal felső Logo kép URL-je",
|
||||
"custom-top-left-corner-logo-link-url": "Egyedi Bal felső Logo Link URL-je",
|
||||
"custom-top-left-corner-logo-height": "Egyedi Bal felső Logo magassága. Alap: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Unggah",
|
||||
"upload-avatar": "Unggah avatar",
|
||||
"uploaded-avatar": "Avatar diunggah",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Carica",
|
||||
"upload-avatar": "Carica un avatar",
|
||||
"uploaded-avatar": "Avatar caricato",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "URL dell'immagine del logo personalizzato nell'angolo superiore sinistro",
|
||||
"custom-top-left-corner-logo-link-url": "URL del link del logo personalizzato nell'angolo superiore sinistro",
|
||||
"custom-top-left-corner-logo-height": "Altezza del logo personalizzato nell'angolo superiore sinistro. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "アップロード",
|
||||
"upload-avatar": "アバターのアップロード",
|
||||
"uploaded-avatar": "アップロードされたアバター",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "カスタムの左上ロゴイメージのURL",
|
||||
"custom-top-left-corner-logo-link-url": "カスタムの左上ロゴイメージのリンクURL",
|
||||
"custom-top-left-corner-logo-height": "カスタムの左上ロゴイメージの高さ。デフォルト:27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "ატვირთვა",
|
||||
"upload-avatar": "სურათის ატვირთვა",
|
||||
"uploaded-avatar": "სურათი ატვირთულია",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
@@ -631,6 +631,9 @@
|
||||
"upload": "Upload",
|
||||
"upload-avatar": "Upload an avatar",
|
||||
"uploaded-avatar": "Uploaded an avatar",
|
||||
"uploading-files": "Uploading files",
|
||||
"upload-failed": "Upload failed",
|
||||
"upload-completed": "Upload completed",
|
||||
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
|
||||
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
|
||||
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
|
||||
|
||||
Loaded 100 of 198 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user