diff --git a/src/App.scss b/src/App.scss
index 3f352e8d9..ef68ac50b 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -645,6 +645,20 @@ option {
}
}
+.cards-list {
+ list-style: none;
+ display: grid;
+ grid-auto-flow: row dense;
+ grid-template-columns: 1fr 1fr;
+
+ li {
+ border: 1px solid var(--border);
+ border-radius: var(--inputRadius);
+ padding: 0.5em;
+ margin: 0.25em;
+ }
+}
+
.btn-block {
display: block;
width: 100%;
@@ -655,16 +669,19 @@ option {
display: inline-flex;
vertical-align: middle;
- button {
+ button,
+ .button-dropdown {
position: relative;
flex: 1 1 auto;
- &:not(:last-child) {
+ &:not(:last-child),
+ &:not(:last-child) .button-default {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
- &:not(:first-child) {
+ &:not(:first-child),
+ &:not(:first-child) .button-default {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js
new file mode 100644
index 000000000..a2c27c2a4
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.js
@@ -0,0 +1,64 @@
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import StringSetting from '../helpers/string_setting.vue'
+import GroupSetting from '../helpers/group_setting.vue'
+import Popover from 'src/components/popover/popover.vue'
+
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faGlobe
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faGlobe
+)
+
+const FrontendsTab = {
+ provide () {
+ return {
+ defaultDraftMode: true,
+ defaultSource: 'admin'
+ }
+ },
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ IntegerSetting,
+ StringSetting,
+ GroupSetting,
+ Popover
+ },
+ created () {
+ if (this.user.rights.admin) {
+ this.$store.dispatch('loadFrontendsStuff')
+ }
+ },
+ computed: {
+ frontends () {
+ return this.$store.state.adminSettings.frontends
+ },
+ ...SharedComputedObject()
+ },
+ methods: {
+ update (frontend, suggestRef) {
+ const ref = suggestRef || frontend.refs[0]
+ const { name } = frontend
+ const payload = { name, ref }
+
+ this.$store.state.api.backendInteractor.installFrontend({ payload })
+ .then((externalUser) => {
+ this.$store.dispatch('loadFrontendsStuff')
+ })
+ },
+ setDefault (frontend, suggestRef) {
+ const ref = suggestRef || frontend.refs[0]
+ const { name } = frontend
+
+ this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } })
+ }
+ }
+}
+
+export default FrontendsTab
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.scss b/src/components/settings_modal/admin_tabs/frontends_tab.scss
new file mode 100644
index 000000000..e3e04bc6f
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.scss
@@ -0,0 +1,13 @@
+.frontends-tab {
+ .cards-list {
+ padding: 0;
+ }
+
+ dd {
+ text-overflow: ellipsis;
+ word-wrap: nowrap;
+ white-space: nowrap;
+ overflow-x: hidden;
+ max-width: 10em;
+ }
+}
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue
new file mode 100644
index 000000000..71bbfa69e
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.vue
@@ -0,0 +1,162 @@
+
+
+
+
{{ $t('admin_dash.tabs.frontends') }}
+
{{ $t('admin_dash.frontend.wip_notice') }}
+
+
+
{{ $t('admin_dash.frontend.available_frontends') }}
+
+ -
+ {{ frontend.name }}
+ {{ ' ' }}
+
+
+
+
+
{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}
+
+
+
+
+ - {{ $t('admin_dash.frontend.repository') }}
+ - {{ frontend.git }}
+
+ - {{ $t('admin_dash.frontend.versions') }}
+ {{ ref }}
+
+ - {{ $t('admin_dash.frontend.build_url') }}
+ - {{ frontend.build_url }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ ' ' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/helpers/group_setting.js b/src/components/settings_modal/helpers/group_setting.js
index 12a490007..23a2a2025 100644
--- a/src/components/settings_modal/helpers/group_setting.js
+++ b/src/components/settings_modal/helpers/group_setting.js
@@ -7,7 +7,6 @@ export default {
computed: {
...Setting.computed,
isDirty () {
- console.log(this.state, this.draft)
return !isEqual(this.state, this.draft)
}
}
diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js
index d2e1a6f43..4f0be448c 100644
--- a/src/components/settings_modal/helpers/setting.js
+++ b/src/components/settings_modal/helpers/setting.js
@@ -61,7 +61,7 @@ export default {
// TODO allow passing shared draft object?
get () {
if (this.realSource === 'admin') {
- return get(this.$store.state.adminSettings.draft, this.path)
+ return get(this.$store.state.adminSettings.draft, this.canonPath)
} else {
return this.localDraft
}
@@ -75,7 +75,7 @@ export default {
}
},
state () {
- const value = get(this.configSource, this.path)
+ const value = get(this.configSource, this.canonPath)
if (value === undefined) {
return this.defaultState
} else {
diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js
index d02db542a..bb3d36ac4 100644
--- a/src/components/settings_modal/helpers/shared_computed_object.js
+++ b/src/components/settings_modal/helpers/shared_computed_object.js
@@ -10,6 +10,9 @@ const SharedComputedObject = () => ({
},
adminConfig () {
return this.$store.state.adminSettings.config
+ },
+ adminDraft () {
+ return this.$store.state.adminSettings.draft
}
})
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index e033d9997..ff58f2c38 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -5,7 +5,7 @@ import getResettableAsyncComponent from 'src/services/resettable_async_component
import Popover from '../popover/popover.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
-import { cloneDeep } from 'lodash'
+import { cloneDeep, isEqual } from 'lodash'
import {
newImporter,
newExporter
@@ -155,6 +155,12 @@ const SettingsModal = {
PLEROMAFE_SETTINGS_MINOR_VERSION
]
return clone
+ },
+ resetAdminDraft () {
+ this.$store.commit('resetAdminDraft')
+ },
+ pushAdminDraft () {
+ this.$store.dispatch('pushAdminDraft')
}
},
computed: {
@@ -183,6 +189,12 @@ const SettingsModal = {
set (value) {
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
}
+ },
+ adminDraftAny () {
+ return !isEqual(
+ this.$store.state.adminSettings.config,
+ this.$store.state.adminSettings.draft
+ )
}
}
}
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 4cce60993..49ef83e01 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -43,7 +43,9 @@
.btn {
min-height: 2em;
- min-width: 10em;
+ }
+
+ .btn:not(.dropdown-button) {
padding: 0 2em;
}
}
@@ -51,6 +53,8 @@
.settings-footer {
display: flex;
+ flex-wrap: wrap;
+ line-height: 2;
>* {
margin-right: 0.5em;
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 303050e43..57ec5535b 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -45,7 +45,7 @@
-
diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js
index 07d4cf2ee..f94721ec7 100644
--- a/src/components/settings_modal/settings_modal_admin_content.js
+++ b/src/components/settings_modal/settings_modal_admin_content.js
@@ -1,15 +1,14 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
-import DataImportExportTab from './tabs/data_import_export_tab.vue'
-import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
import InstanceTab from './admin_tabs/instance_tab.vue'
import LimitsTab from './admin_tabs/limits_tab.vue'
+import FrontendsTab from './admin_tabs/frontends_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faWrench,
faHand,
- faFilter,
+ faLaptopCode,
faPaintBrush,
faBell,
faDownload,
@@ -20,7 +19,7 @@ import {
library.add(
faWrench,
faHand,
- faFilter,
+ faLaptopCode,
faPaintBrush,
faBell,
faDownload,
@@ -32,12 +31,14 @@ const SettingsModalAdminContent = {
components: {
TabSwitcher,
- DataImportExportTab,
- MutesAndBlocksTab,
InstanceTab,
- LimitsTab
+ LimitsTab,
+ FrontendsTab
},
computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ },
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
@@ -46,6 +47,20 @@ const SettingsModalAdminContent = {
},
bodyLock () {
return this.$store.state.interface.settingsModalState === 'visible'
+ },
+ adminDbLoaded () {
+ return this.$store.state.adminSettings.loaded
+ },
+ adminDescriptionsLoaded () {
+ return this.$store.state.adminSettings.descriptions !== null
+ },
+ noDb () {
+ return this.$store.state.adminSettings.dbConfigEnabled === false
+ }
+ },
+ created () {
+ if (this.user.rights.admin) {
+ this.$store.dispatch('loadAdminStuff')
}
},
methods: {
diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue
index 52c6ddf5f..a7a2ac9ad 100644
--- a/src/components/settings_modal/settings_modal_admin_content.vue
+++ b/src/components/settings_modal/settings_modal_admin_content.vue
@@ -1,12 +1,44 @@
+
+
+
+
diff --git a/src/i18n/en.json b/src/i18n/en.json
index f84b5a347..6ae31ee8b 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -842,9 +842,19 @@
"window_title": "Administration",
"wip_notice": "This admin dashboard is experimental and WIP, {adminFeLink}.",
"old_ui_link": "old admin UI available here",
+ "reset_all": "Reset all",
+ "commit_all": "Save all",
"tabs": {
+ "nodb": "No DB Config",
"instance": "Instance",
- "limits": "Limits"
+ "limits": "Limits",
+ "frontends": "Front-ends"
+ },
+ "nodb": {
+ "heading": "Database config is disabled",
+ "text": "You need to change backend config files so that {property} is set to {value}, see more in {documentation}.",
+ "documentation": "documentation",
+ "text2": "Most configuration options will be unavailable."
},
"captcha": {
"native": "Native",
@@ -863,6 +873,23 @@
"users": "User profile limits",
"profile_fields": "Profile fields limits",
"user_uploads": "Profile media limits"
+ },
+ "frontend": {
+ "repository": "Repository link",
+ "versions": "Available versions",
+ "build_url": "Build URL",
+ "reinstall": "Reinstall",
+ "is_default": "(Default)",
+ "is_default_custom": "(Default, version: {version})",
+ "install": "Install",
+ "install_version": "Install version {version}",
+ "set_default": "Set default",
+ "set_default_version": "Set version {version} as default",
+ "wip_notice": "Please note that this section is a WIP and lacks certain features as backend implementation of front-end management is incomplete.",
+ "default_frontend": "Default front-end",
+ "default_frontend_tip": "Default front-end will be shown to all users. Currently there's no way to for a user to select personal front-end. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.",
+ "default_frontend_tip2": "WIP: Since Pleroma backend doesn't properly list all installed frontends you'll have to enter name and reference manually. List below provides shortcuts to fill the values.",
+ "available_frontends": "Available for install"
}
},
"time": {
diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js
index 27d5b37d3..cad9c0ca4 100644
--- a/src/modules/adminSettings.js
+++ b/src/modules/adminSettings.js
@@ -1,11 +1,14 @@
-import { set, get, cloneDeep } from 'lodash'
+import { set, get, cloneDeep, differenceWith, isEqual, flatten } from 'lodash'
export const defaultState = {
+ frontends: [],
+ loaded: false,
needsReboot: null,
config: null,
modifiedPaths: null,
descriptions: null,
- draft: null
+ draft: null,
+ dbConfigEnabled: null
}
export const newUserFlags = {
@@ -17,7 +20,23 @@ const adminSettingsStorage = {
...cloneDeep(defaultState)
},
mutations: {
+ setInstanceAdminNoDbConfig (state) {
+ state.loaded = false
+ state.dbConfigEnabled = false
+ },
+ setAvailableFrontends (state, { frontends }) {
+ state.frontends = frontends.map(f => {
+ if (f.name === 'pleroma-fe') {
+ f.refs = ['master', 'develop']
+ } else {
+ f.refs = [f.ref]
+ }
+ return f
+ })
+ },
updateAdminSettings (state, { config, modifiedPaths }) {
+ state.loaded = true
+ state.dbConfigEnabled = true
state.config = config
state.modifiedPaths = modifiedPaths
},
@@ -40,9 +59,33 @@ const adminSettingsStorage = {
}
},
actions: {
+ loadFrontendsStuff ({ state, rootState, dispatch, commit }) {
+ rootState.api.backendInteractor.fetchAvailableFrontends()
+ .then(frontends => commit('setAvailableFrontends', { frontends }))
+ },
+ loadAdminStuff ({ state, rootState, dispatch, commit }) {
+ rootState.api.backendInteractor.fetchInstanceDBConfig()
+ .then(backendDbConfig => {
+ if (backendDbConfig.error) {
+ if (backendDbConfig.error.status === 400) {
+ backendDbConfig.error.json().then(errorJson => {
+ if (/configurable_from_database/.test(errorJson.error)) {
+ commit('setInstanceAdminNoDbConfig')
+ }
+ })
+ }
+ } else {
+ dispatch('setInstanceAdminSettings', { backendDbConfig })
+ }
+ })
+ if (state.descriptions === null) {
+ rootState.api.backendInteractor.fetchInstanceConfigDescriptions()
+ .then(backendDescriptions => dispatch('setInstanceAdminDescriptions', { backendDescriptions }))
+ }
+ },
setInstanceAdminSettings ({ state, commit, dispatch }, { backendDbConfig }) {
const config = state.config || {}
- const modifiedPaths = state.modifiedPaths || new Set()
+ const modifiedPaths = new Set()
backendDbConfig.configs.forEach(c => {
const path = [c.group, c.key]
if (c.db) {
@@ -82,6 +125,59 @@ const adminSettingsStorage = {
console.log(descriptions[':pleroma']['Pleroma.Captcha'])
commit('updateAdminDescriptions', { descriptions })
},
+
+ // This action takes draft state, diffs it with live config state and then pushes
+ // only differences between the two. Difference detection only work up to subkey (third) level.
+ pushAdminDraft ({ rootState, state, commit, dispatch }) {
+ // TODO cleanup paths in modifiedPaths
+ const convert = (value) => {
+ if (typeof value !== 'object') {
+ return value
+ } else if (Array.isArray(value)) {
+ return value.map(convert)
+ } else {
+ return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
+ }
+ }
+
+ // Getting all group-keys used in config
+ const allGroupKeys = flatten(
+ Object
+ .entries(state.config)
+ .map(
+ ([group, lv1data]) => Object
+ .keys(lv1data)
+ .map((key) => ({ group, key }))
+ )
+ )
+
+ // Only using group-keys where there are changes detected
+ const changedGroupKeys = allGroupKeys.filter(({ group, key }) => {
+ return !isEqual(state.config[group][key], state.draft[group][key])
+ })
+
+ // Here we take all changed group-keys and get all changed subkeys
+ const changed = changedGroupKeys.map(({ group, key }) => {
+ const config = state.config[group][key]
+ const draft = state.draft[group][key]
+
+ // We convert group-key value into entries arrays
+ const eConfig = Object.entries(config)
+ const eDraft = Object.entries(draft)
+
+ // Then those entries array we diff so only changed subkey entries remain
+ // We use the diffed array to reconstruct the object and then shove it into convert()
+ return ({ group, key, value: convert(Object.fromEntries(differenceWith(eDraft, eConfig, isEqual))) })
+ })
+
+ rootState.api.backendInteractor.pushInstanceDBConfig({
+ payload: {
+ configs: changed
+ }
+ })
+ .then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
+ .then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
+ },
pushAdminSetting ({ rootState, state, commit, dispatch }, { path, value }) {
const [group, key, ...rest] = Array.isArray(path) ? path : path.split(/\./g)
const clone = {} // not actually cloning the entire thing to avoid excessive writes
diff --git a/src/modules/users.js b/src/modules/users.js
index 45cba334a..a23f6d7d6 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -564,12 +564,6 @@ const users = {
user.domainMutes = []
commit('setCurrentUser', user)
commit('setServerSideStorage', user)
- if (user.rights.admin) {
- store.rootState.api.backendInteractor.fetchInstanceDBConfig()
- .then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
- store.rootState.api.backendInteractor.fetchInstanceConfigDescriptions()
- .then(backendDescriptions => dispatch('setInstanceAdminDescriptions', { backendDescriptions }))
- }
commit('addNewUsers', [user])
dispatch('fetchEmoji')
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 073f40a34..56e7de714 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -110,6 +110,8 @@ const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcemen
const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config'
const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions'
+const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends'
+const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install'
const oldfetch = window.fetch
@@ -1693,6 +1695,21 @@ const fetchInstanceConfigDescriptions = ({ credentials }) => {
})
}
+const fetchAvailableFrontends = ({ credentials }) => {
+ return fetch(PLEROMA_ADMIN_FRONTENDS_URL, {
+ headers: authHeaders(credentials)
+ })
+ .then((response) => {
+ if (response.ok) {
+ return response.json()
+ } else {
+ return {
+ error: response
+ }
+ }
+ })
+}
+
const pushInstanceDBConfig = ({ credentials, payload }) => {
return fetch(PLEROMA_ADMIN_CONFIG_URL, {
headers: {
@@ -1714,6 +1731,27 @@ const pushInstanceDBConfig = ({ credentials, payload }) => {
})
}
+const installFrontend = ({ credentials, payload }) => {
+ return fetch(PLEROMA_ADMIN_FRONTENDS_INSTALL_URL, {
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ ...authHeaders(credentials)
+ },
+ method: 'POST',
+ body: JSON.stringify(payload)
+ })
+ .then((response) => {
+ if (response.ok) {
+ return response.json()
+ } else {
+ return {
+ error: response
+ }
+ }
+ })
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
@@ -1830,7 +1868,9 @@ const apiService = {
adminFetchAnnouncements,
fetchInstanceDBConfig,
fetchInstanceConfigDescriptions,
- pushInstanceDBConfig
+ fetchAvailableFrontends,
+ pushInstanceDBConfig,
+ installFrontend
}
export default apiService