@conduction/nextcloud-vue 2.120.0 → 2.122.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +53 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +546 -523
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +9 -0
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +1 -0
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js +9 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js +214 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js +125 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js +9 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js +163 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js +131 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js +127 -0
- package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js +9 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js +82 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js +61 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js +60 -0
- package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js +10 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js +226 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js +126 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +42 -32
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +116 -16
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +42 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +42 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +309 -13
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +14136 -12578
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +309 -13
- package/l10n/en.json +42 -1
- package/l10n/nl.json +42 -1
- package/package.json +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +74 -3
- package/src/components/CnIndexPage/useNamedSource.js +9 -0
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +1 -0
- package/src/components/CnReadinessChecklist/CnReadinessChecklist.vue +335 -0
- package/src/components/CnReadinessChecklist/index.js +9 -0
- package/src/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue +300 -0
- package/src/components/CnSchemaRulesWidget/index.js +10 -0
- package/src/components/CnSchemaRulesWidget/schemaRules.js +123 -0
- package/src/components/CnSearchIndexStatus/CnSearchIndexStatus.vue +160 -0
- package/src/components/CnSearchIndexStatus/index.js +10 -0
- package/src/components/CnSearchIndexStatus/searchIndexStatus.js +56 -0
- package/src/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue +328 -0
- package/src/components/CnShippedConfigurationSection/index.js +9 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +10 -0
- package/src/components/index.js +4 -0
- package/src/composables/indexSources.js +116 -16
- package/src/css/index-page.css +7 -0
- package/src/css/look-board-detail.css +27 -0
- package/src/css/look-board-index.css +108 -12
- package/src/css/look-board.css +27 -1
- package/src/index.js +4 -0
- package/src/utils/libraryWidgetKeys.js +1 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* The rules OpenRegister's engine holds for a schema, read rather than kept:
|
|
6
|
+
* the closed sets (vocabulary), the inventory in evaluation order, and a dry
|
|
7
|
+
* run that writes nothing.
|
|
8
|
+
*
|
|
9
|
+
* A rule is keyed on its id (`<kind>:<schemaSlug>:<key>`), never on its
|
|
10
|
+
* position: the order belongs to the pipeline and moves the day a kind is
|
|
11
|
+
* added. Kind labels come from the vocabulary endpoint, so a kind added later
|
|
12
|
+
* renders instead of blanking.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import axios from '@nextcloud/axios'
|
|
16
|
+
import { generateUrl } from '@nextcloud/router'
|
|
17
|
+
|
|
18
|
+
/** Where OpenRegister publishes the three closed sets. */
|
|
19
|
+
export const RULE_VOCABULARY_PATH = '/apps/openregister/api/rules/vocabulary'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} schema The schema slug.
|
|
23
|
+
* @return {string} The url of the schema's rule inventory.
|
|
24
|
+
*/
|
|
25
|
+
export function ruleInventoryUrl(schema) {
|
|
26
|
+
return generateUrl(`/apps/openregister/api/schemas/${encodeURIComponent(schema)}/rules`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} schema The schema slug.
|
|
31
|
+
* @param {string} ruleId The rule id.
|
|
32
|
+
* @return {string} The url of the rule's dry run.
|
|
33
|
+
*/
|
|
34
|
+
export function ruleEvaluateUrl(schema, ruleId) {
|
|
35
|
+
return generateUrl(`/apps/openregister/api/schemas/${encodeURIComponent(schema)}/rules/${encodeURIComponent(ruleId)}/evaluate`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {unknown} data An answer.
|
|
40
|
+
* @return {boolean} Whether it carries the three sets.
|
|
41
|
+
*/
|
|
42
|
+
export function isRuleVocabulary(data) {
|
|
43
|
+
return !!data && Array.isArray(data.kinds) && Array.isArray(data.verdicts) && Array.isArray(data.actions)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The kinds the engine publishes, keyed by kind.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} vocabulary The vocabulary answer.
|
|
50
|
+
* @return {object} The descriptors, by kind.
|
|
51
|
+
*/
|
|
52
|
+
export function kindsByName(vocabulary) {
|
|
53
|
+
const kinds = {}
|
|
54
|
+
if (!isRuleVocabulary(vocabulary)) {
|
|
55
|
+
return kinds
|
|
56
|
+
}
|
|
57
|
+
vocabulary.kinds.forEach((kind) => {
|
|
58
|
+
const name = String((kind && kind.kind) ?? '')
|
|
59
|
+
if (name !== '') {
|
|
60
|
+
kinds[name] = kind
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
return kinds
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read the vocabulary. A refusal answers the empty sets: the vocabulary is open
|
|
68
|
+
* to any signed-in caller, so a refusal means an instance that predates the
|
|
69
|
+
* engine, and each rule then shows its own kind string.
|
|
70
|
+
*
|
|
71
|
+
* @return {Promise<object>} The vocabulary, possibly empty.
|
|
72
|
+
*/
|
|
73
|
+
export async function fetchRuleVocabulary() {
|
|
74
|
+
try {
|
|
75
|
+
const { data } = await axios.get(generateUrl(RULE_VOCABULARY_PATH))
|
|
76
|
+
return isRuleVocabulary(data) ? data : { kinds: [], verdicts: [], actions: [] }
|
|
77
|
+
} catch {
|
|
78
|
+
return { kinds: [], verdicts: [], actions: [] }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read one schema's rules, in the order the engine evaluates them. The
|
|
84
|
+
* inventory is an admin read; a refusal throws for the caller to say so.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} schema The schema slug.
|
|
87
|
+
* @return {Promise<Array<object>>} The rules.
|
|
88
|
+
*/
|
|
89
|
+
export async function fetchSchemaRules(schema) {
|
|
90
|
+
const { data } = await axios.get(ruleInventoryUrl(schema))
|
|
91
|
+
return Array.isArray(data && data.rules) ? data.rules : []
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Try one rule against one stored object, writing nothing.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} schema The schema slug.
|
|
98
|
+
* @param {string} ruleId The rule id.
|
|
99
|
+
* @param {string} register The register slug the object lives in.
|
|
100
|
+
* @param {string} objectId The object to try it against.
|
|
101
|
+
* @return {Promise<object>} The trial result, carrying `trace`.
|
|
102
|
+
*/
|
|
103
|
+
export async function evaluateRule(schema, ruleId, register, objectId) {
|
|
104
|
+
const { data } = await axios.post(ruleEvaluateUrl(schema, ruleId), { register, objectId })
|
|
105
|
+
return data || {}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The trace of a trial. `operand` and `operandValue` are what turn "why did this
|
|
110
|
+
* rule not fire" into a fact, so they are shown beside the verdict.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} result The trial result.
|
|
113
|
+
* @return {{verdict: string, operand: string, operandValue: string, message: string}} The trace.
|
|
114
|
+
*/
|
|
115
|
+
export function traceOf(result) {
|
|
116
|
+
const trace = (result && result.trace) || {}
|
|
117
|
+
return {
|
|
118
|
+
verdict: String(trace.verdict ?? ''),
|
|
119
|
+
operand: String(trace.operand ?? ''),
|
|
120
|
+
operandValue: String(trace.operandValue ?? ''),
|
|
121
|
+
message: String(trace.message ?? (result && result.error && result.error.message) ?? ''),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<div class="cn-search-index-status" data-testid="cn-search-index-status">
|
|
7
|
+
<NcLoadingIcon v-if="loading" :size="24" :name="t('nextcloud-vue', 'Loading')" />
|
|
8
|
+
<NcNoteCard v-else-if="error" type="error" data-testid="cn-search-index-error">
|
|
9
|
+
{{ error }}
|
|
10
|
+
</NcNoteCard>
|
|
11
|
+
<template v-else>
|
|
12
|
+
<dl class="cn-search-index-status__figures">
|
|
13
|
+
<div>
|
|
14
|
+
<dt>{{ t('nextcloud-vue', 'Tables in scope') }}</dt>
|
|
15
|
+
<dd data-testid="cn-search-index-tables">
|
|
16
|
+
{{ status.tableCount }}
|
|
17
|
+
</dd>
|
|
18
|
+
</div>
|
|
19
|
+
<div>
|
|
20
|
+
<dt>{{ t('nextcloud-vue', 'Indexes on them') }}</dt>
|
|
21
|
+
<dd data-testid="cn-search-index-indexes">
|
|
22
|
+
{{ status.indexCount }}
|
|
23
|
+
</dd>
|
|
24
|
+
</div>
|
|
25
|
+
<div>
|
|
26
|
+
<dt>{{ t('nextcloud-vue', 'Last run') }}</dt>
|
|
27
|
+
<dd data-testid="cn-search-index-last-run">
|
|
28
|
+
{{ lastRun }}
|
|
29
|
+
</dd>
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
<dt>{{ t('nextcloud-vue', 'Rebuild without locking') }}</dt>
|
|
33
|
+
<dd data-testid="cn-search-index-concurrent">
|
|
34
|
+
{{ concurrent }}
|
|
35
|
+
</dd>
|
|
36
|
+
</div>
|
|
37
|
+
</dl>
|
|
38
|
+
<p data-testid="cn-search-index-commands">
|
|
39
|
+
{{ t('nextcloud-vue', 'Run {command} status on the server for the same figures, and {command} rebuild --apply after a schema change.', { command }) }}
|
|
40
|
+
</p>
|
|
41
|
+
</template>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
|
|
47
|
+
import { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
|
|
48
|
+
import { fetchSearchIndexStatus, SEARCH_INDEX_COMMAND, SEARCH_INDEX_STATUS_URL } from './searchIndexStatus.js'
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* CnSearchIndexStatus: the state of OpenRegister's search indexes (tables in
|
|
52
|
+
* scope, indexes on them, the last maintenance run, whether a rebuild locks a
|
|
53
|
+
* table), read live and never copied. The acts are named, not offered: a
|
|
54
|
+
* rebuild walks every table in scope and nothing stops one from a settings
|
|
55
|
+
* page, so the occ verbs are printed for somebody with a shell.
|
|
56
|
+
*
|
|
57
|
+
* @spec openspec/changes/search-index-status/specs/search-index-status/spec.md
|
|
58
|
+
*/
|
|
59
|
+
export default {
|
|
60
|
+
name: 'CnSearchIndexStatus',
|
|
61
|
+
|
|
62
|
+
components: { NcLoadingIcon, NcNoteCard },
|
|
63
|
+
|
|
64
|
+
props: {
|
|
65
|
+
/** The app path OpenRegister reports its indexes at. */
|
|
66
|
+
source: { type: String, default: SEARCH_INDEX_STATUS_URL },
|
|
67
|
+
/** The occ command that acts on the indexes. */
|
|
68
|
+
command: { type: String, default: SEARCH_INDEX_COMMAND },
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
data() {
|
|
72
|
+
return {
|
|
73
|
+
loading: true,
|
|
74
|
+
error: '',
|
|
75
|
+
status: { concurrentRebuildSupported: false, tables: {}, tableCount: 0, indexCount: 0, lastRun: null },
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
computed: {
|
|
80
|
+
/** @return {string} When maintenance last ran, or that it never has. */
|
|
81
|
+
lastRun() {
|
|
82
|
+
if (!this.status.lastRun) {
|
|
83
|
+
return t('nextcloud-vue', 'Never')
|
|
84
|
+
}
|
|
85
|
+
const when = new Date(this.status.lastRun)
|
|
86
|
+
if (Number.isNaN(when.getTime())) {
|
|
87
|
+
return String(this.status.lastRun)
|
|
88
|
+
}
|
|
89
|
+
return when.toLocaleString(getCanonicalLocale(), { dateStyle: 'medium', timeStyle: 'short' })
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/** @return {string} Whether a rebuild locks the table, in words. */
|
|
93
|
+
concurrent() {
|
|
94
|
+
return this.status.concurrentRebuildSupported === true
|
|
95
|
+
? t('nextcloud-vue', 'Yes')
|
|
96
|
+
: t('nextcloud-vue', 'No, a rebuild locks the table it is on')
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
mounted() {
|
|
101
|
+
this.load()
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
methods: {
|
|
105
|
+
t,
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Read what OpenRegister says about its indexes.
|
|
109
|
+
*
|
|
110
|
+
* @return {Promise<void>}
|
|
111
|
+
*/
|
|
112
|
+
async load() {
|
|
113
|
+
this.loading = true
|
|
114
|
+
this.error = ''
|
|
115
|
+
try {
|
|
116
|
+
this.status = await fetchSearchIndexStatus(this.source)
|
|
117
|
+
} catch (e) {
|
|
118
|
+
this.error = String((e && e.message) || e)
|
|
119
|
+
} finally {
|
|
120
|
+
this.loading = false
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<style scoped>
|
|
128
|
+
/* Label above value, and the label wraps: Nextcloud's settings stylesheet gives a `dt` a fixed width. */
|
|
129
|
+
.cn-search-index-status__figures {
|
|
130
|
+
display: grid;
|
|
131
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
132
|
+
gap: 16px 24px;
|
|
133
|
+
margin-block: 0 12px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.cn-search-index-status__figures > div {
|
|
137
|
+
display: flex;
|
|
138
|
+
flex-direction: column;
|
|
139
|
+
gap: 2px;
|
|
140
|
+
min-width: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.cn-search-index-status__figures dt {
|
|
144
|
+
display: block;
|
|
145
|
+
float: none;
|
|
146
|
+
width: auto;
|
|
147
|
+
margin: 0;
|
|
148
|
+
padding: 0;
|
|
149
|
+
text-align: start;
|
|
150
|
+
white-space: normal;
|
|
151
|
+
overflow-wrap: anywhere;
|
|
152
|
+
color: var(--color-text-maxcontrast);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.cn-search-index-status__figures dd {
|
|
156
|
+
font-weight: bold;
|
|
157
|
+
margin: 0;
|
|
158
|
+
overflow-wrap: anywhere;
|
|
159
|
+
}
|
|
160
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import CnSearchIndexStatus from './CnSearchIndexStatus.vue'
|
|
7
|
+
|
|
8
|
+
export default CnSearchIndexStatus
|
|
9
|
+
export { CnSearchIndexStatus }
|
|
10
|
+
export { lastRunTime, SEARCH_INDEX_COMMAND } from './searchIndexStatus.js'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import axios from '@nextcloud/axios'
|
|
7
|
+
import { generateUrl } from '@nextcloud/router'
|
|
8
|
+
|
|
9
|
+
/** The occ command that rebuilds, snapshots, restores and reports the indexes. */
|
|
10
|
+
export const SEARCH_INDEX_COMMAND = 'occ openregister:tables:search-index'
|
|
11
|
+
|
|
12
|
+
/** Where OpenRegister reports its search indexes. */
|
|
13
|
+
export const SEARCH_INDEX_STATUS_URL = '/apps/openregister/api/settings/search-index'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* When the last stored run finished, or started, or null when none ran.
|
|
17
|
+
*
|
|
18
|
+
* OpenRegister answers `lastRun` as the report of that run (`startedAt`,
|
|
19
|
+
* `finishedAt`) and as `[]` when nothing ran.
|
|
20
|
+
*
|
|
21
|
+
* @param {object|Array|string|null|undefined} report What OpenRegister answered as `lastRun`.
|
|
22
|
+
* @return {string|null} An ISO 8601 time, or null when no run is on record.
|
|
23
|
+
*/
|
|
24
|
+
export function lastRunTime(report) {
|
|
25
|
+
if (typeof report === 'string') {
|
|
26
|
+
return report === '' ? null : report
|
|
27
|
+
}
|
|
28
|
+
if (report === null || typeof report !== 'object' || Array.isArray(report)) {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
const time = report.finishedAt || report.startedAt || ''
|
|
32
|
+
return typeof time === 'string' && time !== '' ? time : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What OpenRegister says about the indexes behind object search. A failed
|
|
37
|
+
* read throws: an outage and an instance with no indexes look the same from
|
|
38
|
+
* the browser, and only one of them is somebody's problem.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} [url] The app path to read.
|
|
41
|
+
* @return {Promise<{concurrentRebuildSupported: boolean, tables: object, tableCount: number, indexCount: number, lastRun: (string|null)}>}
|
|
42
|
+
* @throws {Error} When it cannot be read.
|
|
43
|
+
*/
|
|
44
|
+
export async function fetchSearchIndexStatus(url = SEARCH_INDEX_STATUS_URL) {
|
|
45
|
+
const { data } = await axios.get(generateUrl(url))
|
|
46
|
+
if (data === null || typeof data !== 'object' || data.error !== undefined) {
|
|
47
|
+
throw new Error(String((data && data.error) ?? 'The search index status could not be read.'))
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
concurrentRebuildSupported: data.concurrentRebuildSupported === true,
|
|
51
|
+
tables: data.tables && typeof data.tables === 'object' ? data.tables : {},
|
|
52
|
+
tableCount: Number(data.tableCount ?? 0),
|
|
53
|
+
indexCount: Number(data.indexCount ?? 0),
|
|
54
|
+
lastRun: lastRunTime(data.lastRun),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<div class="cn-shipped-configuration" data-testid="cn-shipped-configuration">
|
|
7
|
+
<p v-if="description" class="cn-shipped-configuration__intro">
|
|
8
|
+
{{ text(description) }}
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<NcSelect v-if="schemas.length > 1"
|
|
12
|
+
v-model="schemaOption"
|
|
13
|
+
class="cn-shipped-configuration__picker"
|
|
14
|
+
:options="schemaOptions"
|
|
15
|
+
:inputLabel="t('nextcloud-vue', 'Show')"
|
|
16
|
+
:clearable="false"
|
|
17
|
+
data-testid="cn-shipped-schema" />
|
|
18
|
+
|
|
19
|
+
<table class="cn-shipped-configuration__table" data-testid="cn-shipped-configuration-table">
|
|
20
|
+
<thead>
|
|
21
|
+
<tr>
|
|
22
|
+
<th scope="col">
|
|
23
|
+
{{ t('nextcloud-vue', 'Name') }}
|
|
24
|
+
</th>
|
|
25
|
+
<th scope="col">
|
|
26
|
+
{{ t('nextcloud-vue', 'State') }}
|
|
27
|
+
</th>
|
|
28
|
+
<th scope="col">
|
|
29
|
+
{{ t('nextcloud-vue', 'Shipped set') }}
|
|
30
|
+
</th>
|
|
31
|
+
<th scope="col">
|
|
32
|
+
{{ t('nextcloud-vue', 'Version') }}
|
|
33
|
+
</th>
|
|
34
|
+
<th scope="col">
|
|
35
|
+
{{ t('nextcloud-vue', 'Newer version') }}
|
|
36
|
+
</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody>
|
|
40
|
+
<tr v-for="row in rows" :key="row.targetObject">
|
|
41
|
+
<td>{{ row.title || row.targetObject }}</td>
|
|
42
|
+
<td :data-testid="'cn-shipped-state-' + row.targetObject">
|
|
43
|
+
{{ stateLabel(row) }}
|
|
44
|
+
</td>
|
|
45
|
+
<td>{{ row.set }}</td>
|
|
46
|
+
<td>{{ row.setVersion }}</td>
|
|
47
|
+
<td>
|
|
48
|
+
<NcButton v-if="updatable(row)"
|
|
49
|
+
:data-testid="'cn-shipped-adopt-' + row.targetObject"
|
|
50
|
+
:disabled="adopting === row.targetObject"
|
|
51
|
+
@click="adopt(row)">
|
|
52
|
+
{{ adoptLabel(row) }}
|
|
53
|
+
</NcButton>
|
|
54
|
+
<span v-else class="cn-shipped-configuration__muted">
|
|
55
|
+
{{ t('nextcloud-vue', 'Up to date') }}
|
|
56
|
+
</span>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
<tr v-if="!rows.length">
|
|
60
|
+
<td colspan="5"
|
|
61
|
+
class="cn-shipped-configuration__muted"
|
|
62
|
+
data-testid="cn-shipped-configuration-empty">
|
|
63
|
+
{{ emptyLine }}
|
|
64
|
+
</td>
|
|
65
|
+
</tr>
|
|
66
|
+
</tbody>
|
|
67
|
+
</table>
|
|
68
|
+
|
|
69
|
+
<NcNoteCard v-if="loadError" type="error" data-testid="cn-shipped-configuration-error">
|
|
70
|
+
{{ loadError }}
|
|
71
|
+
</NcNoteCard>
|
|
72
|
+
<NcNoteCard v-if="adoptError" type="error" data-testid="cn-shipped-configuration-adopt-error">
|
|
73
|
+
{{ adoptError }}
|
|
74
|
+
</NcNoteCard>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script>
|
|
79
|
+
import axios from '@nextcloud/axios'
|
|
80
|
+
import { showSuccess } from '@nextcloud/dialogs'
|
|
81
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
82
|
+
import { generateUrl } from '@nextcloud/router'
|
|
83
|
+
import { NcButton, NcNoteCard, NcSelect } from '@nextcloud/vue'
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The states a seeded object can be in.
|
|
87
|
+
*
|
|
88
|
+
* @type {Readonly<Record<string, string>>}
|
|
89
|
+
*/
|
|
90
|
+
export const SHIPPED_STATES = Object.freeze({
|
|
91
|
+
shipped: 'shipped',
|
|
92
|
+
changed: 'changed',
|
|
93
|
+
local: 'local',
|
|
94
|
+
removed: 'removed',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* CnShippedConfigurationSection: what an app shipped, what this instance
|
|
99
|
+
* changed, and what it authored. One row per seeded object with the set and
|
|
100
|
+
* version it came from, its state, and the newer version offered per object.
|
|
101
|
+
* Taking a newer version over a local change says so on the button, because
|
|
102
|
+
* the change goes.
|
|
103
|
+
*
|
|
104
|
+
* The app's endpoint answers `GET {endpoint}/{schema}` with `{ items: [{
|
|
105
|
+
* targetObject, title, state, set, setVersion, latestVersion, updateAvailable
|
|
106
|
+
* }] }` and takes `POST {endpoint}/{schema}/{targetObject}/adopt` with `{ set,
|
|
107
|
+
* acceptLocalChangeLoss }`, refusing with `{ reason }`. An empty ledger beside
|
|
108
|
+
* a register that holds objects is said as such (`countUrl`), not as "nothing
|
|
109
|
+
* seeded".
|
|
110
|
+
*
|
|
111
|
+
* @spec openspec/changes/shipped-configuration-section/specs/shipped-configuration-section/spec.md
|
|
112
|
+
*/
|
|
113
|
+
export default {
|
|
114
|
+
name: 'CnShippedConfigurationSection',
|
|
115
|
+
|
|
116
|
+
components: { NcButton, NcNoteCard, NcSelect },
|
|
117
|
+
|
|
118
|
+
inject: {
|
|
119
|
+
/** The host app's translate, for the texts the page declares. */
|
|
120
|
+
cnTranslate: { default: () => (key) => key },
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
props: {
|
|
124
|
+
/** The app path of the shipped ledger (`/apps/<app>/api/starter/shipped`). */
|
|
125
|
+
endpoint: { type: String, required: true },
|
|
126
|
+
/** The schemas to show, `[{ slug, label }]`; the first is shown first. */
|
|
127
|
+
schemas: { type: Array, default: () => [] },
|
|
128
|
+
/**
|
|
129
|
+
* An app path that counts a schema's objects, `{schema}` filled in
|
|
130
|
+
* (`/apps/openregister/api/objects/<register>/{schema}`), read with
|
|
131
|
+
* `_limit=1` for its `total`.
|
|
132
|
+
*/
|
|
133
|
+
countUrl: { type: String, default: '' },
|
|
134
|
+
/** A line above the table. */
|
|
135
|
+
description: { type: String, default: '' },
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
data() {
|
|
139
|
+
return {
|
|
140
|
+
schema: this.schemas.length > 0 ? String(this.schemas[0].slug) : '',
|
|
141
|
+
rows: [],
|
|
142
|
+
present: null,
|
|
143
|
+
loadError: '',
|
|
144
|
+
adoptError: '',
|
|
145
|
+
adopting: '',
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
computed: {
|
|
150
|
+
/** @return {Array<{id: string, label: string}>} The picker's options. */
|
|
151
|
+
schemaOptions() {
|
|
152
|
+
return this.schemas.map((option) => ({ id: String(option.slug), label: this.text(option.label || option.slug) }))
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
schemaOption: {
|
|
156
|
+
/** @return {object|null} The chosen schema. */
|
|
157
|
+
get() {
|
|
158
|
+
return this.schemaOptions.find((option) => option.id === this.schema) || null
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/** @param {object|null} option The chosen schema. */
|
|
162
|
+
set(option) {
|
|
163
|
+
if (option) {
|
|
164
|
+
this.schema = option.id
|
|
165
|
+
this.reload()
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/** @return {string} What the empty table says, which depends on what the register holds. */
|
|
171
|
+
emptyLine() {
|
|
172
|
+
if (typeof this.present === 'number' && this.present > 0) {
|
|
173
|
+
return t('nextcloud-vue', 'The register holds {count} of these. None came from a shipped set, so there is no shipped version to compare with.', { count: this.present })
|
|
174
|
+
}
|
|
175
|
+
return t('nextcloud-vue', 'Nothing has been seeded yet.')
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
mounted() {
|
|
180
|
+
this.reload()
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
methods: {
|
|
184
|
+
t,
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param {string} value A declared text.
|
|
188
|
+
* @return {string} It, translated by the host.
|
|
189
|
+
*/
|
|
190
|
+
text(value) {
|
|
191
|
+
return value ? String(this.cnTranslate(String(value))) : ''
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Read what shipped for the shown schema. A failed read empties the
|
|
196
|
+
* table: rows left beside an error would read as the current state.
|
|
197
|
+
*
|
|
198
|
+
* @return {Promise<void>}
|
|
199
|
+
*/
|
|
200
|
+
async reload() {
|
|
201
|
+
if (!this.schema) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const { data } = await axios.get(generateUrl(`${this.endpoint}/${encodeURIComponent(this.schema)}`))
|
|
206
|
+
this.rows = Array.isArray(data && data.items) ? data.items : []
|
|
207
|
+
this.loadError = ''
|
|
208
|
+
this.present = null
|
|
209
|
+
if (this.rows.length === 0) {
|
|
210
|
+
this.present = await this.count()
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
this.rows = []
|
|
214
|
+
this.present = null
|
|
215
|
+
this.loadError = t('nextcloud-vue', 'Could not read the shipped configuration.')
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* How many objects of the shown schema the register holds, whatever
|
|
221
|
+
* brought them.
|
|
222
|
+
*
|
|
223
|
+
* @return {Promise<number|null>} The count, or null when it cannot be read.
|
|
224
|
+
*/
|
|
225
|
+
async count() {
|
|
226
|
+
if (!this.countUrl) {
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const { data } = await axios.get(generateUrl(this.countUrl.replace('{schema}', encodeURIComponent(this.schema))), { params: { _limit: 1 } })
|
|
231
|
+
const total = Number(data && data.total)
|
|
232
|
+
return Number.isFinite(total) ? total : null
|
|
233
|
+
} catch {
|
|
234
|
+
return null
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @param {object} row A ledger row.
|
|
240
|
+
* @return {string} What an administrator reads beside the object.
|
|
241
|
+
*/
|
|
242
|
+
stateLabel(row) {
|
|
243
|
+
const state = (row && row.state) || SHIPPED_STATES.local
|
|
244
|
+
return {
|
|
245
|
+
[SHIPPED_STATES.shipped]: t('nextcloud-vue', 'Shipped, unchanged'),
|
|
246
|
+
[SHIPPED_STATES.changed]: t('nextcloud-vue', 'Shipped, changed here'),
|
|
247
|
+
[SHIPPED_STATES.removed]: t('nextcloud-vue', 'Shipped, removed here'),
|
|
248
|
+
}[state] || t('nextcloud-vue', 'Yours')
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {object} row A ledger row.
|
|
253
|
+
* @return {boolean} Whether a newer shipped version can be taken.
|
|
254
|
+
*/
|
|
255
|
+
updatable(row) {
|
|
256
|
+
return Boolean(row && row.updateAvailable && row.state !== SHIPPED_STATES.removed)
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {object} row A ledger row.
|
|
261
|
+
* @return {boolean} Whether taking the newer version overwrites a local change.
|
|
262
|
+
*/
|
|
263
|
+
losesLocalChange(row) {
|
|
264
|
+
return Boolean(row && row.state === SHIPPED_STATES.changed)
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @param {object} row A ledger row.
|
|
269
|
+
* @return {string} The button's words, which say when a local change goes.
|
|
270
|
+
*/
|
|
271
|
+
adoptLabel(row) {
|
|
272
|
+
return this.losesLocalChange(row)
|
|
273
|
+
? t('nextcloud-vue', 'Take {version} and lose your change', { version: row.latestVersion })
|
|
274
|
+
: t('nextcloud-vue', 'Take {version}', { version: row.latestVersion })
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Take the newer shipped version of one object, then read the ledger again.
|
|
279
|
+
*
|
|
280
|
+
* @param {object} row A ledger row.
|
|
281
|
+
* @return {Promise<void>}
|
|
282
|
+
*/
|
|
283
|
+
async adopt(row) {
|
|
284
|
+
this.adopting = row.targetObject
|
|
285
|
+
this.adoptError = ''
|
|
286
|
+
try {
|
|
287
|
+
await axios.post(
|
|
288
|
+
generateUrl(`${this.endpoint}/${encodeURIComponent(this.schema)}/${encodeURIComponent(row.targetObject)}/adopt`),
|
|
289
|
+
{ set: row.set, acceptLocalChangeLoss: this.losesLocalChange(row) },
|
|
290
|
+
)
|
|
291
|
+
showSuccess(t('nextcloud-vue', 'The newer version was taken.'))
|
|
292
|
+
await this.reload()
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const data = error && error.response && error.response.data
|
|
295
|
+
this.adoptError = String((data && (data.reason || data.message)) || t('nextcloud-vue', 'The newer version was not taken.'))
|
|
296
|
+
} finally {
|
|
297
|
+
this.adopting = ''
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
</script>
|
|
303
|
+
|
|
304
|
+
<style scoped>
|
|
305
|
+
.cn-shipped-configuration__intro,
|
|
306
|
+
.cn-shipped-configuration__muted {
|
|
307
|
+
color: var(--color-text-maxcontrast);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.cn-shipped-configuration__picker {
|
|
311
|
+
max-width: 320px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.cn-shipped-configuration__table {
|
|
315
|
+
width: 100%;
|
|
316
|
+
border-collapse: collapse;
|
|
317
|
+
margin-top: 12px;
|
|
318
|
+
/* Nextcloud sets `table { white-space: nowrap }`; a cell here can be a sentence. */
|
|
319
|
+
white-space: normal;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.cn-shipped-configuration__table th,
|
|
323
|
+
.cn-shipped-configuration__table td {
|
|
324
|
+
padding: 6px 8px;
|
|
325
|
+
border-bottom: 1px solid var(--color-border);
|
|
326
|
+
text-align: start;
|
|
327
|
+
}
|
|
328
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import CnShippedConfigurationSection from './CnShippedConfigurationSection.vue'
|
|
7
|
+
|
|
8
|
+
export default CnShippedConfigurationSection
|
|
9
|
+
export { CnShippedConfigurationSection }
|