Commit 10df1b4b authored by Nick's avatar Nick

feat: storage actions + git module actions

parent 16d88a7c
...@@ -158,6 +158,24 @@ ...@@ -158,6 +158,24 @@
.caption.mt-3 Currently set to every #[strong {{getDefaultSchedule(tgt.syncInterval)}}]. .caption.mt-3 Currently set to every #[strong {{getDefaultSchedule(tgt.syncInterval)}}].
.caption The default is every #[strong {{getDefaultSchedule(tgt.syncIntervalDefault)}}]. .caption The default is every #[strong {{getDefaultSchedule(tgt.syncIntervalDefault)}}].
template(v-if='tgt.actions && tgt.actions.length > 0')
v-divider.mt-3
v-subheader.pl-0 Actions
v-container.pt-0(grid-list-xl, fluid)
v-layout(row, wrap, fill-height)
v-flex(xs12, lg6, xl4, v-for='act of tgt.actions')
v-card.radius-7.grey(flat, :class='$vuetify.dark ? `darken-3-d5` : `lighten-3`', height='100%')
v-card-text
.subheading(v-html='act.label')
.body-1.mt-2(v-html='act.hint')
v-btn.mx-0.mt-3(
@click='executeAction(tgt.key, act.handler)'
outline
:color='$vuetify.dark ? `blue` : `primary`'
:disabled='runningAction'
:loading='runningActionHandler === act.handler'
) Run
</template> </template>
<script> <script>
...@@ -170,6 +188,7 @@ import { LoopingRhombusesSpinner } from 'epic-spinners' ...@@ -170,6 +188,7 @@ import { LoopingRhombusesSpinner } from 'epic-spinners'
import statusQuery from 'gql/admin/storage/storage-query-status.gql' import statusQuery from 'gql/admin/storage/storage-query-status.gql'
import targetsQuery from 'gql/admin/storage/storage-query-targets.gql' import targetsQuery from 'gql/admin/storage/storage-query-targets.gql'
import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'
import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql' import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
momentDurationFormatSetup(moment) momentDurationFormatSetup(moment)
...@@ -184,6 +203,8 @@ export default { ...@@ -184,6 +203,8 @@ export default {
}, },
data() { data() {
return { return {
runningAction: false,
runningActionHandler: '',
currentTab: 0, currentTab: 0,
targets: [], targets: [],
status: [] status: []
...@@ -209,12 +230,12 @@ export default { ...@@ -209,12 +230,12 @@ export default {
mutation: targetsSaveMutation, mutation: targetsSaveMutation,
variables: { variables: {
targets: this.targets.map(tgt => _.pick(tgt, [ targets: this.targets.map(tgt => _.pick(tgt, [
'isEnabled', 'isEnabled',
'key', 'key',
'config', 'config',
'mode', 'mode',
'syncInterval' 'syncInterval'
])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))})) ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
} }
}) })
this.currentTab = 0 this.currentTab = 0
...@@ -239,6 +260,30 @@ export default { ...@@ -239,6 +260,30 @@ export default {
}, },
getDefaultSchedule(val) { getDefaultSchedule(val) {
return moment.duration(val).format('y [years], M [months], d [days], h [hours], m [minutes]') return moment.duration(val).format('y [years], M [months], d [days], h [hours], m [minutes]')
},
async executeAction(targetKey, handler) {
this.$store.commit(`loadingStart`, 'admin-storage-executeaction')
this.runningAction = true
this.runningActionHandler = handler
try {
await this.$apollo.mutate({
mutation: targetExecuteActionMutation,
variables: {
targetKey,
handler
}
})
this.$store.commit('showNotification', {
message: 'Action completed.',
style: 'success',
icon: 'check'
})
} catch (err) {
console.warn(err)
}
this.runningAction = false
this.runningActionHandler = ''
this.$store.commit(`loadingStop`, 'admin-storage-executeaction')
} }
}, },
apollo: { apollo: {
......
mutation($targetKey: String!, $handler: String!) {
storage {
executeAction(targetKey: $targetKey, handler: $handler) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
...@@ -17,6 +17,11 @@ query { ...@@ -17,6 +17,11 @@ query {
key key
value value
} }
actions {
handler
label
hint
}
} }
} }
} }
...@@ -77,6 +77,16 @@ module.exports = { ...@@ -77,6 +77,16 @@ module.exports = {
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
},
async executeAction(obj, args, context) {
try {
await WIKI.models.storage.executeAction(args.targetKey, args.handler)
return {
responseResult: graphHelper.generateSuccess('Action completed.')
}
} catch (err) {
return graphHelper.generateError(err)
}
} }
} }
} }
...@@ -31,6 +31,11 @@ type StorageMutation { ...@@ -31,6 +31,11 @@ type StorageMutation {
updateTargets( updateTargets(
targets: [StorageTargetInput]! targets: [StorageTargetInput]!
): DefaultResponse @auth(requires: ["manage:system"]) ): DefaultResponse @auth(requires: ["manage:system"])
executeAction(
targetKey: String!
handler: String!
): DefaultResponse @auth(requires: ["manage:system"])
} }
# ----------------------------------------------- # -----------------------------------------------
...@@ -51,6 +56,7 @@ type StorageTarget { ...@@ -51,6 +56,7 @@ type StorageTarget {
syncInterval: String syncInterval: String
syncIntervalDefault: String syncIntervalDefault: String
config: [KeyValuePair] config: [KeyValuePair]
actions: [StorageTargetAction]
} }
input StorageTargetInput { input StorageTargetInput {
...@@ -68,3 +74,9 @@ type StorageStatus { ...@@ -68,3 +74,9 @@ type StorageStatus {
message: String! message: String!
lastAttempt: String! lastAttempt: String!
} }
type StorageTargetAction {
handler: String!
label: String!
hint: String!
}
...@@ -36,5 +36,25 @@ module.exports = { ...@@ -36,5 +36,25 @@ module.exports = {
*/ */
generateHash(opts) { generateHash(opts) {
return crypto.createHash('sha1').update(`${opts.locale}|${opts.path}|${opts.privateNS}`).digest('hex') return crypto.createHash('sha1').update(`${opts.locale}|${opts.path}|${opts.privateNS}`).digest('hex')
},
/**
* Inject Page Metadata
*/
injectPageMetadata(page) {
let meta = [
['title', page.title],
['description', page.description],
['published', page.isPublished.toString()],
['date', page.updatedAt],
['tags', '']
]
switch (page.contentType) {
case 'markdown':
return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + page.content
case 'html':
return '<!--\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n-->\n\n' + page.content
default:
return page.content
}
} }
} }
...@@ -130,21 +130,7 @@ module.exports = class Page extends Model { ...@@ -130,21 +130,7 @@ module.exports = class Page extends Model {
* Inject page metadata into contents * Inject page metadata into contents
*/ */
injectMetadata () { injectMetadata () {
let meta = [ return pageHelper.injectPageMetadata(this)
['title', this.title],
['description', this.description],
['published', this.isPublished.toString()],
['date', this.updatedAt],
['tags', '']
]
switch (this.contentType) {
case 'markdown':
return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + this.content
case 'html':
return '<!--\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n-->\n\n' + this.content
default:
return this.content
}
} }
/** /**
......
...@@ -60,7 +60,7 @@ module.exports = class Storage extends Model { ...@@ -60,7 +60,7 @@ module.exports = class Storage extends Model {
newTargets.push({ newTargets.push({
key: target.key, key: target.key,
isEnabled: false, isEnabled: false,
mode: target.defaultMode || 'push', mode: target.defaultMode || 'push',
syncInterval: target.schedule || 'P0D', syncInterval: target.schedule || 'P0D',
config: _.transform(target.props, (result, value, key) => { config: _.transform(target.props, (result, value, key) => {
_.set(result, key, value.default) _.set(result, key, value.default)
...@@ -116,7 +116,7 @@ module.exports = class Storage extends Model { ...@@ -116,7 +116,7 @@ module.exports = class Storage extends Model {
} }
// -> Initialize targets // -> Initialize targets
for(let target of this.targets) { for (let target of this.targets) {
const targetDef = _.find(WIKI.data.storage, ['key', target.key]) const targetDef = _.find(WIKI.data.storage, ['key', target.key])
target.fn = require(`../modules/storage/${target.key}/storage`) target.fn = require(`../modules/storage/${target.key}/storage`)
target.fn.config = target.config target.fn.config = target.config
...@@ -161,7 +161,7 @@ module.exports = class Storage extends Model { ...@@ -161,7 +161,7 @@ module.exports = class Storage extends Model {
static async pageEvent({ event, page }) { static async pageEvent({ event, page }) {
try { try {
for(let target of this.targets) { for (let target of this.targets) {
await target.fn[event](page) await target.fn[event](page)
} }
} catch (err) { } catch (err) {
...@@ -169,4 +169,22 @@ module.exports = class Storage extends Model { ...@@ -169,4 +169,22 @@ module.exports = class Storage extends Model {
throw err throw err
} }
} }
static async executeAction(targetKey, handler) {
try {
const target = _.find(this.targets, ['key', targetKey])
if (target) {
if (_.has(target.fn, handler)) {
await target.fn[handler]()
} else {
throw new Error('Invalid Handler for Storage Target')
}
} else {
throw new Error('Invalid or Inactive Storage Target')
}
} catch (err) {
WIKI.logger.warn(err)
throw err
}
}
} }
...@@ -77,4 +77,10 @@ props: ...@@ -77,4 +77,10 @@ props:
actions: actions:
- handler: syncUntracked - handler: syncUntracked
label: Add Untracked Changes label: Add Untracked Changes
hint: Output all content from the DB to the Git repo to ensure all untracked content is saved. If you enabled Git after content was created or you temporarily disabled Git, you'll want to execute this action to add the missing untracked changes. hint: Output all content from the DB to the local Git repository to ensure all untracked content is saved. If you enabled Git after content was created or you temporarily disabled Git, you'll want to execute this action to add the missing untracked changes.
- handler: sync
label: Force Sync
hint: Will trigger an immediate sync operation, regardless of the current sync schedule. The sync direction is respected.
- handler: importAll
label: Import Everything
hint: Will import all content currently in the local Git repository, regardless of the latest commit state. Useful for importing content from the remote repository created before git was enabled.
...@@ -2,6 +2,11 @@ const path = require('path') ...@@ -2,6 +2,11 @@ const path = require('path')
const sgit = require('simple-git/promise') const sgit = require('simple-git/promise')
const fs = require('fs-extra') const fs = require('fs-extra')
const _ = require('lodash') const _ = require('lodash')
const stream = require('stream')
const Promise = require('bluebird')
const pipeline = Promise.promisify(stream.pipeline)
const klaw = require('klaw')
const pageHelper = require('../../../helpers/page.js')
const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\/)?(.*)/i const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\/)?(.*)/i
...@@ -154,66 +159,74 @@ module.exports = { ...@@ -154,66 +159,74 @@ module.exports = {
const diff = await this.git.diffSummary(['-M', currentCommitLog.hash, latestCommitLog.hash]) const diff = await this.git.diffSummary(['-M', currentCommitLog.hash, latestCommitLog.hash])
if (_.get(diff, 'files', []).length > 0) { if (_.get(diff, 'files', []).length > 0) {
for (const item of diff.files) { await this.processFiles(diff.files)
const contentType = getContenType(item.file) }
if (!contentType) { }
continue },
} /**
const contentPath = getPagePath(item.file) * Process Files
*
* @param {Array<String>} files Array of files to process
*/
async processFiles(files) {
for (const item of files) {
const contentType = getContenType(item.file)
if (!contentType) {
continue
}
const contentPath = getPagePath(item.file)
let itemContents = '' let itemContents = ''
try { try {
itemContents = await fs.readFile(path.join(this.repoPath, item.file), 'utf8') itemContents = await fs.readFile(path.join(this.repoPath, item.file), 'utf8')
const pageData = WIKI.models.pages.parseMetadata(itemContents, contentType) const pageData = WIKI.models.pages.parseMetadata(itemContents, contentType)
const currentPage = await WIKI.models.pages.query().findOne({ const currentPage = await WIKI.models.pages.query().findOne({
path: contentPath.path, path: contentPath.path,
localeCode: contentPath.locale localeCode: contentPath.locale
}) })
if (currentPage) { if (currentPage) {
// Already in the DB, can mark as modified // Already in the DB, can mark as modified
WIKI.logger.info(`(STORAGE/GIT) Page marked as modified: ${item.file}`) WIKI.logger.info(`(STORAGE/GIT) Page marked as modified: ${item.file}`)
await WIKI.models.pages.updatePage({ await WIKI.models.pages.updatePage({
id: currentPage.id, id: currentPage.id,
title: _.get(pageData, 'title', currentPage.title), title: _.get(pageData, 'title', currentPage.title),
description: _.get(pageData, 'description', currentPage.description), description: _.get(pageData, 'description', currentPage.description),
isPublished: _.get(pageData, 'isPublished', currentPage.isPublished), isPublished: _.get(pageData, 'isPublished', currentPage.isPublished),
isPrivate: false, isPrivate: false,
content: pageData.content, content: pageData.content,
authorId: 1, authorId: 1,
skipStorage: true skipStorage: true
}) })
} else { } else {
// Not in the DB, can mark as new // Not in the DB, can mark as new
WIKI.logger.info(`(STORAGE/GIT) Page marked as new: ${item.file}`) WIKI.logger.info(`(STORAGE/GIT) Page marked as new: ${item.file}`)
const pageEditor = await WIKI.models.editors.getDefaultEditor(contentType) const pageEditor = await WIKI.models.editors.getDefaultEditor(contentType)
await WIKI.models.pages.createPage({ await WIKI.models.pages.createPage({
path: contentPath.path, path: contentPath.path,
locale: contentPath.locale, locale: contentPath.locale,
title: _.get(pageData, 'title', _.last(contentPath.path.split('/'))), title: _.get(pageData, 'title', _.last(contentPath.path.split('/'))),
description: _.get(pageData, 'description', ''), description: _.get(pageData, 'description', ''),
isPublished: _.get(pageData, 'isPublished', true), isPublished: _.get(pageData, 'isPublished', true),
isPrivate: false, isPrivate: false,
content: pageData.content, content: pageData.content,
authorId: 1, authorId: 1,
editor: pageEditor, editor: pageEditor,
skipStorage: true skipStorage: true
}) })
} }
} catch (err) { } catch (err) {
if (err.code === 'ENOENT' && item.deletions > 0 && item.insertions === 0) { if (err.code === 'ENOENT' && item.deletions > 0 && item.insertions === 0) {
// File was deleted by git, can safely mark as deleted in DB // File was deleted by git, can safely mark as deleted in DB
WIKI.logger.info(`(STORAGE/GIT) Page marked as deleted: ${item.file}`) WIKI.logger.info(`(STORAGE/GIT) Page marked as deleted: ${item.file}`)
await WIKI.models.pages.deletePage({ await WIKI.models.pages.deletePage({
path: contentPath.path, path: contentPath.path,
locale: contentPath.locale, locale: contentPath.locale,
skipStorage: true skipStorage: true
}) })
} else { } else {
WIKI.logger.warn(`(STORAGE/GIT) Failed to open ${item.file}`) WIKI.logger.warn(`(STORAGE/GIT) Failed to open ${item.file}`)
WIKI.logger.warn(err) WIKI.logger.warn(err)
}
}
} }
} }
} }
...@@ -278,5 +291,56 @@ module.exports = { ...@@ -278,5 +291,56 @@ module.exports = {
await this.git.commit(`docs: rename ${page.sourcePath} to ${destinationFilePath}`, destinationFilePath, { await this.git.commit(`docs: rename ${page.sourcePath} to ${destinationFilePath}`, destinationFilePath, {
'--author': `"${page.authorName} <${page.authorEmail}>"` '--author': `"${page.authorName} <${page.authorEmail}>"`
}) })
},
/**
* HANDLERS
*/
async importAll() {
WIKI.logger.info(`(STORAGE/GIT) Importing all content from local Git repo to the DB...`)
await pipeline(
klaw(this.repoPath, {
filter: (f) => {
return !_.includes(f, '.git')
}
}),
new stream.Transform({
objectMode: true,
transform: async (file, enc, cb) => {
const relPath = file.path.substr(this.repoPath.length + 1)
if (relPath && relPath.length > 3) {
WIKI.logger.info(`(STORAGE/GIT) Processing ${relPath}...`)
await this.processFiles([{
file: relPath,
deletions: 0,
insertions: 0
}])
}
cb()
}
})
)
WIKI.logger.info('(STORAGE/GIT) Import completed.')
},
async syncUntracked() {
WIKI.logger.info(`(STORAGE/GIT) Adding all untracked content...`)
await pipeline(
WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'contentType', 'content', 'isPublished', 'updatedAt').select().from('pages').where({
isPrivate: false
}).stream(),
new stream.Transform({
objectMode: true,
transform: async (page, enc, cb) => {
const fileName = `${page.path}.${getFileExtension(page.contentType)}`
WIKI.logger.info(`(STORAGE/GIT) Adding ${fileName}...`)
const filePath = path.join(this.repoPath, fileName)
await fs.outputFile(filePath, pageHelper.injectPageMetadata(page), 'utf8')
await this.git.add(`./${fileName}`)
cb()
}
})
)
await this.git.commit(`docs: add all untracked content`)
WIKI.logger.info('(STORAGE/GIT) All content is now tracked.')
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment