mirror of
https://github.com/nextcloud/all-in-one.git
synced 2025-12-20 14:36:52 +00:00
Merge pull request #1044 from nextcloud/enh/1036/fulltextsearch
add fulltextsearch as option
This commit is contained in:
commit
40efd3092f
18 changed files with 157 additions and 5 deletions
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
|
|
@ -144,3 +144,12 @@ updates:
|
||||||
labels:
|
labels:
|
||||||
- 3. to review
|
- 3. to review
|
||||||
- dependencies
|
- dependencies
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/Containers/fulltextsearch"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
time: "12:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
labels:
|
||||||
|
- 3. to review
|
||||||
|
- dependencies
|
||||||
|
|
|
||||||
4
Containers/fulltextsearch/Dockerfile
Normal file
4
Containers/fulltextsearch/Dockerfile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Probably from here https://github.com/elastic/elasticsearch/blob/main/distribution/docker/src/docker/Dockerfile
|
||||||
|
FROM elasticsearch:7.17.5
|
||||||
|
|
||||||
|
RUN elasticsearch /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch ingest-attachment
|
||||||
|
|
@ -238,7 +238,8 @@ RUN set -ex; \
|
||||||
chmod +r /upgrade.exclude && \
|
chmod +r /upgrade.exclude && \
|
||||||
chmod +x /cron.sh && \
|
chmod +x /cron.sh && \
|
||||||
chmod +x /notify.sh && \
|
chmod +x /notify.sh && \
|
||||||
chmod +x /activate-collabora.sh
|
chmod +x /activate-collabora.sh && \
|
||||||
|
chmod +x /activate-fulltextsearch.sh
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
mkdir /mnt/ncdata; \
|
mkdir /mnt/ncdata; \
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
if [ "$COLLABORA_ENABLED" != yes ]; then
|
if [ "$COLLABORA_ENABLED" != yes ]; then
|
||||||
# Basically sleep for forever if collabora is not enabled
|
# Basically sleep for forever if collabora is not enabled
|
||||||
sleep 365d
|
sleep inf
|
||||||
fi
|
fi
|
||||||
while ! nc -z "$NC_DOMAIN" 443; do
|
while ! nc -z "$NC_DOMAIN" 443; do
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
@ -10,4 +10,4 @@ done
|
||||||
sleep 10
|
sleep 10
|
||||||
echo "Activating collabora config..."
|
echo "Activating collabora config..."
|
||||||
php /var/www/html/occ richdocuments:activate-config
|
php /var/www/html/occ richdocuments:activate-config
|
||||||
sleep 365d
|
sleep inf
|
||||||
|
|
|
||||||
9
Containers/nextcloud/activate-fulltextsearch.sh
Normal file
9
Containers/nextcloud/activate-fulltextsearch.sh
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "$FULLTEXTSEARCH_ENABLED" != yes ]; then
|
||||||
|
# Basically sleep for forever if fulltextsearch is not enabled
|
||||||
|
sleep inf
|
||||||
|
fi
|
||||||
|
echo "Activating fulltextsearch..."
|
||||||
|
php /var/www/html/occ fulltextsearch:live -q
|
||||||
|
sleep inf
|
||||||
|
|
@ -434,5 +434,47 @@ if version_greater "$installed_version" "24.0.0.0"; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Fulltextsearch
|
||||||
|
if [ "$FULLTEXTSEARCH_ENABLED" = 'yes' ]; then
|
||||||
|
while ! nc -z "$FULLTEXTSEARCH_HOST" 9200; do
|
||||||
|
echo "waiting for Fulltextsearch to become available..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
if ! [ -d "/var/www/html/custom_apps/fulltextsearch" ]; then
|
||||||
|
php /var/www/html/occ app:install fulltextsearch
|
||||||
|
elif [ "$(php /var/www/html/occ config:app:get fulltextsearch enabled)" = "no" ]; then
|
||||||
|
php /var/www/html/occ app:enable fulltextsearch
|
||||||
|
else
|
||||||
|
php /var/www/html/occ app:update fulltextsearch
|
||||||
|
fi
|
||||||
|
if ! [ -d "/var/www/html/custom_apps/fulltextsearch_elasticsearch" ]; then
|
||||||
|
php /var/www/html/occ app:install fulltextsearch_elasticsearch
|
||||||
|
elif [ "$(php /var/www/html/occ config:app:get fulltextsearch_elasticsearch enabled)" = "no" ]; then
|
||||||
|
php /var/www/html/occ app:enable fulltextsearch_elasticsearch
|
||||||
|
else
|
||||||
|
php /var/www/html/occ app:update fulltextsearch_elasticsearch
|
||||||
|
fi
|
||||||
|
if ! [ -d "/var/www/html/custom_apps/files_fulltextsearch" ]; then
|
||||||
|
php /var/www/html/occ app:install files_fulltextsearch
|
||||||
|
elif [ "$(php /var/www/html/occ config:app:get files_fulltextsearch enabled)" = "no" ]; then
|
||||||
|
php /var/www/html/occ app:enable files_fulltextsearch
|
||||||
|
else
|
||||||
|
php /var/www/html/occ app:update files_fulltextsearch
|
||||||
|
fi
|
||||||
|
php /var/www/html/occ fulltextsearch:configure '{"search_platform":"OCA\\FullTextSearch_Elasticsearch\\Platform\\ElasticSearchPlatform"}'
|
||||||
|
php /var/www/html/occ fulltextsearch_elasticsearch:configure "{\"elastic_host\":\"http://$FULLTEXTSEARCH_HOST:9200\"}"
|
||||||
|
php /var/www/html/occ files_fulltextsearch:configure "{\"files_pdf\":\"1\",\"files_office\":\"1\"}"
|
||||||
|
else
|
||||||
|
if [ -d "/var/www/html/custom_apps/fulltextsearch" ]; then
|
||||||
|
php /var/www/html/occ app:remove fulltextsearch
|
||||||
|
fi
|
||||||
|
if [ -d "/var/www/html/custom_apps/fulltextsearch_elasticsearch" ]; then
|
||||||
|
php /var/www/html/occ app:remove fulltextsearch_elasticsearch
|
||||||
|
fi
|
||||||
|
if [ -d "/var/www/html/custom_apps/files_fulltextsearch" ]; then
|
||||||
|
php /var/www/html/occ app:remove files_fulltextsearch
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Remove the update skip file always
|
# Remove the update skip file always
|
||||||
rm -f /mnt/ncdata/skip.update
|
rm -f /mnt/ncdata/skip.update
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,10 @@ stdout_logfile_maxbytes=0
|
||||||
stderr_logfile=/dev/stderr
|
stderr_logfile=/dev/stderr
|
||||||
stderr_logfile_maxbytes=0
|
stderr_logfile_maxbytes=0
|
||||||
command=/activate-collabora.sh
|
command=/activate-collabora.sh
|
||||||
|
|
||||||
|
[program:activate-fulltextsearch]
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
command=/activate-fulltextsearch.sh
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
{
|
{
|
||||||
"identifier": "nextcloud-aio-apache",
|
"identifier": "nextcloud-aio-apache",
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
|
"nextcloud-aio-fulltextsearch",
|
||||||
"nextcloud-aio-onlyoffice",
|
"nextcloud-aio-onlyoffice",
|
||||||
"nextcloud-aio-collabora",
|
"nextcloud-aio-collabora",
|
||||||
"nextcloud-aio-clamav",
|
"nextcloud-aio-clamav",
|
||||||
|
|
@ -144,7 +145,9 @@
|
||||||
"TALK_PORT=%TALK_PORT%",
|
"TALK_PORT=%TALK_PORT%",
|
||||||
"IMAGINARY_ENABLED=%IMAGINARY_ENABLED%",
|
"IMAGINARY_ENABLED=%IMAGINARY_ENABLED%",
|
||||||
"IMAGINARY_HOST=nextcloud-aio-imaginary",
|
"IMAGINARY_HOST=nextcloud-aio-imaginary",
|
||||||
"PHP_UPLOAD_LIMIT=%NEXTCLOUD_UPLOAD_LIMIT%"
|
"PHP_UPLOAD_LIMIT=%NEXTCLOUD_UPLOAD_LIMIT%",
|
||||||
|
"FULLTEXTSEARCH_ENABLED=%FULLTEXTSEARCH_ENABLED%",
|
||||||
|
"FULLTEXTSEARCH_HOST=nextcloud-aio-fulltextsearch"
|
||||||
],
|
],
|
||||||
"maxShutdownTime": 10,
|
"maxShutdownTime": 10,
|
||||||
"restartPolicy": "unless-stopped"
|
"restartPolicy": "unless-stopped"
|
||||||
|
|
@ -388,6 +391,31 @@
|
||||||
"secrets": [],
|
"secrets": [],
|
||||||
"maxShutdownTime": 10,
|
"maxShutdownTime": 10,
|
||||||
"restartPolicy": "unless-stopped"
|
"restartPolicy": "unless-stopped"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "nextcloud-aio-fulltextsearch",
|
||||||
|
"dependsOn": [],
|
||||||
|
"displayName": "Fulltextsearch",
|
||||||
|
"containerName": "nextcloud/aio-fulltextsearch",
|
||||||
|
"ports": [],
|
||||||
|
"internalPorts": [
|
||||||
|
"9200"
|
||||||
|
],
|
||||||
|
"environmentVariables": [
|
||||||
|
"TZ=%TIMEZONE%",
|
||||||
|
"discovery.type=single-node",
|
||||||
|
"ES_JAVA_OPTS=-Xms1024M -Xmx1024M"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"name": "nextcloud_aio_elasticsearch",
|
||||||
|
"location": "/usr/share/elasticsearch/data",
|
||||||
|
"writeable": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secrets": [],
|
||||||
|
"maxShutdownTime": 10,
|
||||||
|
"restartPolicy": "unless-stopped"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
php/public/disable-fulltextsearch.js
Normal file
5
php/public/disable-fulltextsearch.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
// Fulltextsearch
|
||||||
|
var fulltextsearch = document.getElementById("fulltextsearch");
|
||||||
|
fulltextsearch.disabled = true;
|
||||||
|
});
|
||||||
|
|
@ -103,6 +103,7 @@ $app->get('/containers', function ($request, $response, $args) use ($container)
|
||||||
'automatic_updates' => $configurationManager->areAutomaticUpdatesEnabled(),
|
'automatic_updates' => $configurationManager->areAutomaticUpdatesEnabled(),
|
||||||
'is_backup_section_enabled' => $configurationManager->isBackupSectionEnabled(),
|
'is_backup_section_enabled' => $configurationManager->isBackupSectionEnabled(),
|
||||||
'is_imaginary_enabled' => $configurationManager->isImaginaryEnabled(),
|
'is_imaginary_enabled' => $configurationManager->isImaginaryEnabled(),
|
||||||
|
'is_fulltextsearch_enabled' => $configurationManager->isFulltextsearchEnabled(),
|
||||||
]);
|
]);
|
||||||
})->setName('profile');
|
})->setName('profile');
|
||||||
$app->get('/login', function ($request, $response, $args) use ($container) {
|
$app->get('/login', function ($request, $response, $args) use ($container) {
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,8 @@ document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
// Imaginary
|
// Imaginary
|
||||||
var imaginary = document.getElementById("imaginary");
|
var imaginary = document.getElementById("imaginary");
|
||||||
imaginary.addEventListener('change', makeOptionsFormSubmitVisible);
|
imaginary.addEventListener('change', makeOptionsFormSubmitVisible);
|
||||||
|
|
||||||
|
// Fulltextsearch
|
||||||
|
var fulltextsearch = document.getElementById("fulltextsearch");
|
||||||
|
fulltextsearch.addEventListener('change', makeOptionsFormSubmitVisible);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ class ContainerDefinitionFetcher
|
||||||
if (!$this->configurationManager->isImaginaryEnabled()) {
|
if (!$this->configurationManager->isImaginaryEnabled()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
} elseif ($entry['identifier'] === 'nextcloud-aio-fulltextsearch') {
|
||||||
|
if (!$this->configurationManager->isFulltextsearchEnabled()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$ports = new ContainerPorts();
|
$ports = new ContainerPorts();
|
||||||
|
|
@ -154,6 +158,10 @@ class ContainerDefinitionFetcher
|
||||||
if (!$this->configurationManager->isImaginaryEnabled()) {
|
if (!$this->configurationManager->isImaginaryEnabled()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
} elseif ($value === 'nextcloud-aio-fulltextsearch') {
|
||||||
|
if (!$this->configurationManager->isFulltextsearchEnabled()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$dependsOn[] = $value;
|
$dependsOn[] = $value;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,11 @@ class ConfigurationController
|
||||||
} else {
|
} else {
|
||||||
$this->configurationManager->SetImaginaryEnabledState(0);
|
$this->configurationManager->SetImaginaryEnabledState(0);
|
||||||
}
|
}
|
||||||
|
if (isset($request->getParsedBody()['fulltextsearch'])) {
|
||||||
|
$this->configurationManager->SetFulltextsearchEnabledState(1);
|
||||||
|
} else {
|
||||||
|
$this->configurationManager->SetFulltextsearchEnabledState(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($request->getParsedBody()['delete_collabora_dictionaries'])) {
|
if (isset($request->getParsedBody()['delete_collabora_dictionaries'])) {
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,21 @@ class ConfigurationManager
|
||||||
$this->WriteConfig($config);
|
$this->WriteConfig($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isFulltextsearchEnabled() : bool {
|
||||||
|
$config = $this->GetConfig();
|
||||||
|
if (isset($config['isFulltextsearchEnabled']) && $config['isFulltextsearchEnabled'] === 1) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetFulltextsearchEnabledState(int $value) : void {
|
||||||
|
$config = $this->GetConfig();
|
||||||
|
$config['isFulltextsearchEnabled'] = $value;
|
||||||
|
$this->WriteConfig($config);
|
||||||
|
}
|
||||||
|
|
||||||
public function isOnlyofficeEnabled() : bool {
|
public function isOnlyofficeEnabled() : bool {
|
||||||
$config = $this->GetConfig();
|
$config = $this->GetConfig();
|
||||||
if (isset($config['isOnlyofficeEnabled']) && $config['isOnlyofficeEnabled'] === 1) {
|
if (isset($config['isOnlyofficeEnabled']) && $config['isOnlyofficeEnabled'] === 1) {
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,12 @@ class DockerActionManager
|
||||||
} else {
|
} else {
|
||||||
$replacements[1] = '';
|
$replacements[1] = '';
|
||||||
}
|
}
|
||||||
|
} elseif ($out[1] === 'FULLTEXTSEARCH_ENABLED') {
|
||||||
|
if ($this->configurationManager->isFulltextsearchEnabled()) {
|
||||||
|
$replacements[1] = 'yes';
|
||||||
|
} else {
|
||||||
|
$replacements[1] = '';
|
||||||
|
}
|
||||||
} elseif ($out[1] === 'NEXTCLOUD_UPLOAD_LIMIT') {
|
} elseif ($out[1] === 'NEXTCLOUD_UPLOAD_LIMIT') {
|
||||||
$replacements[1] = $this->configurationManager->GetNextcloudUploadLimit();
|
$replacements[1] = $this->configurationManager->GetNextcloudUploadLimit();
|
||||||
} elseif ($out[1] === 'NEXTCLOUD_MAX_TIME') {
|
} elseif ($out[1] === 'NEXTCLOUD_MAX_TIME') {
|
||||||
|
|
|
||||||
|
|
@ -452,6 +452,11 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<input type="checkbox" id="collabora" name="collabora"><label for="collabora">Collabora (Nextcloud Office)</label><br>
|
<input type="checkbox" id="collabora" name="collabora"><label for="collabora">Collabora (Nextcloud Office)</label><br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if is_fulltextsearch_enabled == true %}
|
||||||
|
<input type="checkbox" id="fulltextsearch" name="fulltextsearch" checked="checked"><label for="fulltextsearch">Fulltextsearch (needs ~1GB additional RAM)</label><br>
|
||||||
|
{% else %}
|
||||||
|
<input type="checkbox" id="fulltextsearch" name="fulltextsearch"><label for="fulltextsearch">Fulltextsearch (needs ~1GB additional RAM)</label><br>
|
||||||
|
{% endif %}
|
||||||
{% if is_imaginary_enabled == true %}
|
{% if is_imaginary_enabled == true %}
|
||||||
<input type="checkbox" id="imaginary" name="imaginary" checked="checked"><label for="imaginary">Imaginary</label><br>
|
<input type="checkbox" id="imaginary" name="imaginary" checked="checked"><label for="imaginary">Imaginary</label><br>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
@ -469,7 +474,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="options-form-submit" class="button" type="submit" value="Save changes" />
|
<input id="options-form-submit" class="button" type="submit" value="Save changes" />
|
||||||
</form>
|
</form>
|
||||||
<b>Minimal system requirements:</b> When any optional addon is enabled, at least 2GB RAM, a dual-core CPU and 40GB system storage are required. When enabling ClamAV, at least 3GB RAM are required. Recommended are at least 4GB of RAM.<br><br>
|
<b>Minimal system requirements:</b> When any optional addon is enabled, at least 2GB RAM, a dual-core CPU and 40GB system storage are required. When enabling ClamAV or Fulltextsearch, at least 3GB RAM are required. When enabling everything, at least 4GB RAM are required. Recommended are at least 1GB more RAM than the minimal requirement.<br><br>
|
||||||
{% if isAnyRunning == true or is_x64_platform == false %}
|
{% if isAnyRunning == true or is_x64_platform == false %}
|
||||||
<script type="text/javascript" src="disable-clamav.js"></script>
|
<script type="text/javascript" src="disable-clamav.js"></script>
|
||||||
<script type="text/javascript" src="disable-onlyoffice.js"></script>
|
<script type="text/javascript" src="disable-onlyoffice.js"></script>
|
||||||
|
|
@ -478,6 +483,7 @@
|
||||||
<script type="text/javascript" src="disable-talk.js"></script>
|
<script type="text/javascript" src="disable-talk.js"></script>
|
||||||
<script type="text/javascript" src="disable-collabora.js"></script>
|
<script type="text/javascript" src="disable-collabora.js"></script>
|
||||||
<script type="text/javascript" src="disable-imaginary.js"></script>
|
<script type="text/javascript" src="disable-imaginary.js"></script>
|
||||||
|
<script type="text/javascript" src="disable-fulltextsearch.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if is_collabora_enabled == true and isAnyRunning == false and was_start_button_clicked == true %}
|
{% if is_collabora_enabled == true and isAnyRunning == false and was_start_button_clicked == true %}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ Included are:
|
||||||
- Backup solution (based on [BorgBackup](https://github.com/borgbackup/borg#what-is-borgbackup))
|
- Backup solution (based on [BorgBackup](https://github.com/borgbackup/borg#what-is-borgbackup))
|
||||||
- Imaginary
|
- Imaginary
|
||||||
- ClamAV
|
- ClamAV
|
||||||
|
- Fulltextsearch
|
||||||
|
|
||||||
## How to use this?
|
## How to use this?
|
||||||
The following instructions are especially meant for Linux. For macOS see [this](#how-to-run-aio-on-macos), for Windows see [this](#how-to-run-aio-on-windows).
|
The following instructions are especially meant for Linux. For macOS see [this](#how-to-run-aio-on-macos), for Windows see [this](#how-to-run-aio-on-windows).
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
- [ ] Collabora by trying to open a .docx or .odt file in Nextcloud
|
- [ ] Collabora by trying to open a .docx or .odt file in Nextcloud
|
||||||
- [ ] Nextcloud Talk by opening the Talk app in Nextcloud, creating a new chat and trying to join a call in this chat. Also verifying in the settings that the HPB and turn server work.
|
- [ ] Nextcloud Talk by opening the Talk app in Nextcloud, creating a new chat and trying to join a call in this chat. Also verifying in the settings that the HPB and turn server work.
|
||||||
- [ ] Imaginary by having a look if when uploading a new picture in Nextcloud, it adds some log entries to the container
|
- [ ] Imaginary by having a look if when uploading a new picture in Nextcloud, it adds some log entries to the container
|
||||||
|
- [ ] Fulltextsearch by trying to search for a heading inside a file in Nextcloud
|
||||||
- [ ] When Collabora is enabled, it should show below the Optional Addons section a section where you can change the dictionaries for collabora. `de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru` should be a valid setting. E.g. `de.De` not. If already set, it should show a button that allows to remove the setting again.
|
- [ ] When Collabora is enabled, it should show below the Optional Addons section a section where you can change the dictionaries for collabora. `de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru` should be a valid setting. E.g. `de.De` not. If already set, it should show a button that allows to remove the setting again.
|
||||||
|
|
||||||
You can now continue with [060-environmental-variables.md](./060-environmental-variables.md)
|
You can now continue with [060-environmental-variables.md](./060-environmental-variables.md)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue