Commit 8e09c6fc authored by NGPixel's avatar NGPixel

refactor: updated loggers + admin UI improvements + setup fixes

parent 6baa277f
'use strict'
/* global siteConfig */
import CONSTANTS from './constants'
import Vue from 'vue'
......
......@@ -4,13 +4,13 @@
.pa-3.pt-4
.headline.primary--text Authentication
.subheading.grey--text Configure the authentication settings of your wiki
v-tabs(color='grey lighten-4', grow, slider-color='primary', show-arrows)
v-tabs(color='grey lighten-4', fixed-tabs, slider-color='primary', show-arrows)
v-tab(key='settings'): v-icon settings
v-tab(v-for='provider in providers', :key='provider.key') {{ provider.title }}
v-tab(v-for='provider in activeProviders', :key='provider.key') {{ provider.title }}
v-tab-item(key='settings', :transition='false', :reverse-transition='false')
v-card.pa-3
.body-2.pb-2 Select which authentication providers are enabled:
.body-2.pb-2 Select which authentication providers to enable:
v-form
v-checkbox(
v-for='(provider, n) in providers',
......@@ -32,11 +32,24 @@
v-btn(icon, @click='refresh')
v-icon.grey--text refresh
v-tab-item(v-for='(provider, n) in providers', :key='provider.key', :transition='false', :reverse-transition='false')
v-tab-item(v-for='(provider, n) in activeProviders', :key='provider.key', :transition='false', :reverse-transition='false')
v-card.pa-3
.body-1(v-if='!provider.props || provider.props.length < 1') This provider has no configuration options you can modify.
v-form(v-else)
v-text-field(v-for='prop in provider.props', :key='prop', :label='prop', prepend-icon='mode_edit')
v-form
v-subheader Provider Configuration
.body-1(v-if='!provider.props || provider.props.length < 1') This provider has no configuration options you can modify.
v-text-field(v-else, v-for='prop in provider.props', :key='prop', :label='prop', prepend-icon='mode_edit')
v-divider
v-subheader Registration
v-switch.ml-3(
v-model='auths',
label='Allow self-registration',
:value='true',
color='primary',
hint='Allow any user successfully authorized by the provider to access the wiki.',
persistent-hint
)
v-text-field(label='Limit to specific email domains', prepend-icon='mail_outline')
v-text-field(label='Assign to group', prepend-icon='people')
v-divider
v-btn(color='primary')
v-icon(left) chevron_right
......@@ -52,6 +65,8 @@
</template>
<script>
import _ from 'lodash'
/* global CONSTANTS */
export default {
......@@ -62,6 +77,11 @@ export default {
refreshCompleted: false
}
},
computed: {
activeProviders() {
return _.filter(this.providers, 'isEnabled')
}
},
apollo: {
providers: {
query: CONSTANTS.GRAPH.AUTHENTICATION.QUERY_PROVIDERS,
......
......@@ -7,27 +7,48 @@
v-form.pt-3
v-layout(row wrap)
v-flex(lg6 xs12)
v-card
v-toolbar(color='blue', dark, dense, flat)
v-toolbar-title
.subheading Site Info
v-btn(fab, absolute, right, bottom, small, light): v-icon save
v-card-text
v-text-field(label='Site Title', required, :counter='50', v-model='siteTitle')
v-text-field(label='Site Description', :counter='255')
v-text-field(label='Site Keywords', :counter='255')
v-select(label='Meta Robots', chips, tags, :items='metaRobots', v-model='metaRobotsSelection')
v-form
v-card
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title
.subheading Site Info
v-subheader General
.px-3
v-text-field(label='Site Title', required, :counter='50', v-model='siteTitle', prepend-icon='public')
v-divider
v-subheader SEO
.px-3
v-text-field(label='Site Description', :counter='255', prepend-icon='public')
v-text-field(label='Site Keywords', :counter='255', prepend-icon='public')
v-select(label='Meta Robots', chips, tags, :items='metaRobots', v-model='metaRobotsSelection', prepend-icon='public')
v-divider
.px-3.pb-3
v-btn(color='primary') Save
v-flex(lg6 xs12)
v-card
v-toolbar(color='blue', dark, dense, flat)
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title
.subheading Site Branding
v-card-text ---
v-card.mt-3
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title
.subheading Maintenance Mode
v-card-text
.body-1 Maintenance mode restrict access to the site to administrators only, regarless of current permissions.
v-btn.mt-3(color='orange darken-2', dark)
icon-home-alert.mr-2(fillColor='#FFFFFF')
| Turn On Maintenance Mode
</template>
<script>
import IconHomeAlert from 'mdi/home-alert'
export default {
components: {
IconHomeAlert
},
data() {
return {
siteTitle: 'Wiki.js',
......
......@@ -8,10 +8,9 @@
v-layout(row wrap)
v-flex(lg6 xs12)
v-card
v-toolbar(color='blue', dark, dense, flat)
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title
.subheading Locale
v-btn(fab, absolute, right, bottom, small, light): v-icon save
.subheading Locale Settings
v-card-text
v-select(:items='locales', prepend-icon='public', v-model='selectedLocale', label='Site Locale', persistent-hint, hint='All UI text elements will be displayed in selected language.')
template(slot='item', slot-scope='data')
......@@ -21,16 +20,19 @@
v-list-tile-title(v-html='data.item.text')
v-list-tile-sub-title(v-html='data.item.original')
v-divider
v-switch(v-model='rtlEnabled', label='RTL Text Display', color='primary', persistent-hint, hint='For Right-to-Left languages, e.g. Arabic')
v-switch(v-model='rtlEnabled', label='RTL Display Mode', color='primary', persistent-hint, hint='For Right-to-Left languages, e.g. Arabic')
v-divider
.px-3.pb-3
v-btn(color='primary') Save
v-flex(lg6 xs12)
v-card
v-toolbar(color='blue', dark, dense, flat)
v-toolbar(color='teal', dark, dense, flat)
v-toolbar-title
.subheading Download Locale
v-list
v-list-tile(@click='')
v-list-tile-avatar
v-avatar.blue.white--text(tile, size='40') ZH
v-avatar.teal.white--text(tile, size='40') ZH
v-list-tile-content
v-list-tile-title Chinese
v-list-tile-sub-title 中文
......@@ -39,7 +41,7 @@
v-icon.grey--text cloud_download
v-list-tile(@click='')
v-list-tile-avatar
v-avatar.blue.white--text(tile, size='40') EN
v-avatar.teal.white--text(tile, size='40') EN
v-list-tile-content
v-list-tile-title English
v-list-tile-sub-title English
......@@ -47,7 +49,7 @@
v-icon.green--text check
v-list-tile(@click='')
v-list-tile-avatar
v-avatar.blue.white--text(tile, size='40') FR
v-avatar.teal.white--text(tile, size='40') FR
v-list-tile-content
v-list-tile-title French
v-list-tile-sub-title Français
......@@ -55,7 +57,7 @@
v-icon.green--text check
v-list-tile(@click='')
v-list-tile-avatar
v-avatar.blue.white--text(tile, size='40') RU
v-avatar.teal.white--text(tile, size='40') RU
v-list-tile-content
v-list-tile-title Russian
v-list-tile-sub-title Русский
......
<template lang='pug'>
v-card(flat)
v-card(color='grey lighten-5')
.pa-3.pt-4
.headline.primary--text Logging
.subheading.grey--text Configure the system logger(s)
v-tabs(color='grey lighten-4', fixed-tabs, slider-color='primary', show-arrows)
v-tab(key='settings'): v-icon settings
v-tab(v-for='svc in activeServices', :key='svc.key') {{ svc.title }}
v-tab-item(key='settings', :transition='false', :reverse-transition='false')
v-card.pa-3
.body-2.pb-2 Select which logging service to enable:
v-form
v-checkbox(
v-for='(svc, n) in services',
v-model='selectedServices',
:key='svc.key',
:label='svc.title',
:value='svc.key',
color='primary',
:disabled='svc.key === `console`'
hide-details
)
v-divider
v-btn(color='primary')
v-icon(left) chevron_right
| Set Services
v-btn(color='black', dark)
v-icon(left) keyboard
| View Console
v-btn(color='black', dark)
v-icon(left) layers_clear
| Purge Logs
v-btn(icon, @click='refresh')
v-icon.grey--text refresh
v-tab-item(v-for='(svc, n) in activeServices', :key='svc.key', :transition='false', :reverse-transition='false')
v-card.pa-3
v-form
v-subheader Service Configuration
.body-1(v-if='!svc.props || svc.props.length < 1') This logging service has no configuration options you can modify.
v-text-field(v-else, v-for='prop in svc.props', :key='prop', :label='prop', prepend-icon='mode_edit')
v-divider
v-btn(color='primary')
v-icon(left) chevron_right
| Save Configuration
v-snackbar(
color='success'
top
v-model='refreshCompleted'
)
v-icon.mr-3(dark) cached
| List of logging services has been refreshed.
</template>
<script>
import _ from 'lodash'
/* global CONSTANTS */
export default {
data() {
return {
services: [],
selectedServices: ['console'],
refreshCompleted: false
}
},
computed: {
activeServices() {
return _.filter(this.services, 'isEnabled')
}
},
apollo: {
services: {
query: CONSTANTS.GRAPH.AUTHENTICATION.QUERY_PROVIDERS,
update: (data) => data.authentication.providers
}
},
methods: {
async refresh() {
await this.$apollo.queries.services.refetch()
this.refreshCompleted = true
}
}
}
</script>
<style lang='scss'>
</style>
......@@ -4,7 +4,7 @@
.pa-3.pt-4
.headline.primary--text Search Engine
.subheading.grey--text Configure the search capabilities of your wiki
v-tabs(color='grey lighten-4', grow, slider-color='primary', show-arrows)
v-tabs(color='grey lighten-4', fixed-tabs, slider-color='primary', show-arrows)
v-tab(key='settings'): v-icon settings
v-tab(key='db') Database
v-tab(key='algolia') Algolia
......
......@@ -4,7 +4,7 @@
.pa-3.pt-4
.headline.primary--text Storage
.subheading.grey--text Set backup and sync targets for your content
v-tabs(color='grey lighten-4', grow, slider-color='primary', show-arrows)
v-tabs(color='grey lighten-4', fixed-tabs, slider-color='primary', show-arrows)
v-tab(key='settings'): v-icon settings
v-tab(key='local') Local FS
v-tab(key='git') Git
......@@ -19,7 +19,15 @@
v-tab-item(key='settings')
v-card.pa-3
v-form
v-checkbox(v-for='(target, n) in targets', v-model='auths', :key='n', :label='target.text', :value='target.value', color='primary')
v-checkbox(
v-for='(target, n) in targets',
v-model='auths',
:key='n',
:label='target.text',
:value='target.value',
color='primary',
hide-details
)
v-divider
v-btn(color='primary')
v-icon(left) chevron_right
......@@ -32,7 +40,7 @@ export default {
data() {
return {
targets: [
{ text: 'Local FS', value: 'local' },
{ text: 'Local Filesystem', value: 'local' },
{ text: 'Git', value: 'auth0' },
{ text: 'Amazon S3', value: 'algolia' },
{ text: 'Azure Blob Storage', value: 'elasticsearch' },
......
......@@ -8,10 +8,9 @@
v-layout(row wrap)
v-flex(lg6 xs12)
v-card
v-toolbar(color='blue', dark, dense, flat)
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title
.subheading Theme
v-btn(fab, absolute, right, bottom, small, light): v-icon save
v-card-text
v-select(:items='themes', prepend-icon='palette', v-model='selectedTheme', label='Site Theme', persistent-hint, hint='Themes affect how content pages are displayed. Other site sections (such as the editor or admin area) are not affected.')
template(slot='item', slot-scope='data')
......@@ -22,12 +21,15 @@
v-list-tile-sub-title(v-html='data.item.author')
v-divider
v-switch(v-model='darkMode', label='Dark Mode', color='primary', persistent-hint, hint='Not recommended for accessibility')
v-divider
.px-3.pb-3
v-btn(color='primary') Save
v-flex(lg6 xs12)
v-card
v-toolbar(color='blue', dark, dense, flat)
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title
.subheading Theme Options
v-list
.subheading ---
v-card-text ---
</template>
<script>
......
......@@ -85,6 +85,7 @@ const router = new VueRouter({
{ path: '/theme', component: () => import(/* webpackChunkName: "admin" */ './admin-theme.vue') },
{ path: '/users', component: () => import(/* webpackChunkName: "admin" */ './admin-users.vue') },
{ path: '/auth', component: () => import(/* webpackChunkName: "admin" */ './admin-auth.vue') },
{ path: '/logging', component: () => import(/* webpackChunkName: "admin" */ './admin-logging.vue') },
{ path: '/search', component: () => import(/* webpackChunkName: "admin" */ './admin-search.vue') },
{ path: '/storage', component: () => import(/* webpackChunkName: "admin" */ './admin-storage.vue') },
{ path: '/api', component: () => import(/* webpackChunkName: "admin" */ './admin-api.vue') },
......
<template lang='pug'>
v-toolbar(color='black', dark, app, clipped-left, fixed, flat)
v-toolbar-side-icon(@click.native='')
v-icon view_module
v-toolbar-title
span.subheading Wiki.js
v-spacer
transition(name='navHeaderSearch')
v-text-field(
ref='searchField',
v-if='searchIsShown',
v-model='search',
clearable,
color='blue',
label='Search...',
single-line,
hide-details,
append-icon='search',
:append-icon-cb='searchEnter',
:loading='searchIsLoading',
@keyup.enter='searchEnter',
@keyup.esc='searchToggle'
)
v-progress-linear(
indeterminate,
slot='progress',
height='2',
color='blue'
)
v-spacer
v-progress-circular.mr-3(indeterminate, color='blue', v-show='$apollo.loading')
v-btn(icon)
v-icon(color='grey') search
v-btn(icon, @click.native='darkTheme = !darkTheme')
transition(name='navHeaderSearch')
v-btn(icon, @click='searchToggle', v-if='!searchIsShown')
v-icon(color='grey') search
v-btn(icon, href='/a')
v-icon(color='grey') settings
v-menu(offset-y, min-width='300')
v-btn(icon, slot='activator')
......@@ -20,7 +45,10 @@
v-list-tile-title John Doe
v-list-tile-sub-title john.doe@example.com
v-divider.my-0
v-list-tile(@click='')
v-list-tile(href='/p')
v-list-tile-action: v-icon(color='red') person
v-list-tile-title Profile
v-list-tile(href='/logout')
v-list-tile-action: v-icon(color='red') exit_to_app
v-list-tile-title Logout
</template>
......@@ -28,11 +56,41 @@
<script>
export default {
data() {
return {}
return {
searchIsLoading: false,
searchIsShown: false,
search: ''
}
},
methods: {
searchToggle() {
this.searchIsLoading = false
this.searchIsShown = !this.searchIsShown
if (this.searchIsShown) {
this.$nextTick(() => {
this.$refs.searchField.focus()
})
}
},
searchEnter() {
this.searchIsLoading = true
}
}
}
</script>
<style>
<style lang='scss'>
.navHeaderSearch {
&-enter-active, &-leave-active {
transition: opacity .25s ease, transform .25s ease;
opacity: 1;
}
&-enter-active {
transition-delay: .25s;
}
&-enter, &-leave-to {
opacity: 0;
transform: translateY(-25px);
}
}
</style>
......@@ -40,7 +40,7 @@
v-stepper-content(step='1')
v-card.text-xs-center.pa-3(flat)
img(src='svg/logo-wikijs.svg', alt='Wiki.js Logo', style='width: 300px;')
img(src='/svg/logo-wikijs.svg', alt='Wiki.js Logo', style='width: 300px;')
v-container
.body-2.py-2 This installation wizard will guide you through the steps needed to get your wiki up and running in no time!
.body-1
......
......@@ -128,12 +128,12 @@
"request-promise": "4.2.2",
"scim-query-filter-parser": "1.1.0",
"semver": "5.5.0",
"sequelize": "4.35.2",
"sequelize": "4.36.0",
"serve-favicon": "2.4.5",
"uuid": "3.2.1",
"validator": "9.4.1",
"validator-as-promised": "1.0.2",
"winston": "2.4.0",
"winston": "3.0.0-rc2",
"yargs": "11.0.0"
},
"devDependencies": {
......@@ -157,8 +157,8 @@
"brace": "0.11.1",
"cache-loader": "1.2.2",
"clean-webpack-plugin": "0.1.19",
"colors": "1.1.2",
"copy-webpack-plugin": "4.5.0",
"colors": "1.2.0",
"copy-webpack-plugin": "4.5.1",
"css-loader": "0.28.10",
"cssnano": "4.0.0-rc.2",
"duplicate-package-checker-webpack-plugin": "2.1.0",
......@@ -192,15 +192,15 @@
"sass-loader": "6.0.7",
"sass-resources-loader": "1.3.3",
"simple-progress-webpack-plugin": "1.1.2",
"style-loader": "0.20.2",
"style-loader": "0.20.3",
"stylus": "0.54.5",
"stylus-loader": "3.0.2",
"twemoji-awesome": "1.0.6",
"uglifyjs-webpack-plugin": "1.2.2",
"uglifyjs-webpack-plugin": "1.2.3",
"vee-validate": "2.0.5",
"velocity-animate": "1.5.1",
"vue": "2.5.13",
"vue-apollo": "3.0.0-beta.4",
"vue": "2.5.15",
"vue-apollo": "3.0.0-beta.5",
"vue-clipboards": "1.2.2",
"vue-codemirror": "4.0.3",
"vue-hot-reload-api": "2.3.0",
......@@ -208,8 +208,8 @@
"vue-material-design-icons": "1.2.1",
"vue-router": "3.0.1",
"vue-simple-breakpoints": "1.0.3",
"vue-template-compiler": "2.5.13",
"vuetify": "1.0.5",
"vue-template-compiler": "2.5.15",
"vuetify": "1.0.6",
"vuex": "3.0.1",
"vuex-persistedstate": "2.4.2",
"webpack": "3.11.0",
......
......@@ -53,7 +53,6 @@ defaults:
configNamespaces:
- auth
- features
- git
- logging
- site
- theme
......
......@@ -33,12 +33,11 @@ module.exports = {
// Load authentication strategies
const modules = _.values(autoload(path.join(WIKI.SERVERPATH, 'modules/authentication')))
console.info(WIKI.config.auth)
_.forEach(modules, (strategy) => {
const strategyConfig = _.get(WIKI.config.auth.strategies, strategy.key, {})
const strategyConfig = _.get(WIKI.config.auth.strategies, strategy.key, { isEnabled: false })
strategyConfig.callbackURL = `${WIKI.config.site.host}${WIKI.config.site.path}login/${strategy.key}/callback`
strategy.config = strategyConfig
if (strategyConfig.isEnabled) {
console.info(strategy.title)
try {
strategy.init(passport, strategyConfig)
} catch (err) {
......
const _ = require('lodash')
const cluster = require('cluster')
const fs = require('fs-extra')
const path = require('path')
const winston = require('winston')
/* global WIKI */
module.exports = {
loggers: {},
init() {
let winston = require('winston')
let logger = new (winston.Logger)({
let logger = winston.createLogger({
level: WIKI.config.logLevel,
transports: []
})
logger.filters.push((level, msg) => {
let processName = (cluster.isMaster) ? 'MASTER' : `WORKER-${cluster.worker.id}`
return '[' + processName + '] ' + msg
format: winston.format.combine(
winston.format.colorize(),
winston.format.label({ label: (cluster.isMaster) ? 'MASTER' : `WORKER-${cluster.worker.id}` }),
winston.format.timestamp(),
winston.format.printf(info => `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`)
)
})
_.forOwn(_.omitBy(WIKI.config.logging.loggers, s => s.enabled === false), (loggerConfig, loggerKey) => {
let loggerModule = require(`../modules/logging/${loggerKey}`)
loggerModule.init(logger, loggerConfig)
fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${loggerKey}.svg`), 'utf8').then(iconData => {
logger.icon = iconData
}).catch(err => {
if (err.code === 'ENOENT') {
logger.icon = '[missing icon]'
} else {
logger.error(err)
}
})
this.loggers[logger.key] = loggerModule
})
......
......@@ -17,7 +17,7 @@ module.exports = {
AuthenticationQuery: {
providers(obj, args, context, info) {
let prv = _.map(WIKI.auth.strategies, str => ({
isEnabled: true,
isEnabled: str.config.isEnabled,
key: str.key,
props: str.props,
title: str.title,
......
......@@ -24,9 +24,9 @@ module.exports = {
callback(null, true)
}
logger.add(BugsnagLogger, {
logger.add(new BugsnagLogger({
level: 'warn',
key: conf.key
})
}))
}
}
......@@ -11,12 +11,12 @@ module.exports = {
title: 'Console',
props: [],
init (logger, conf) {
logger.add(winston.transports.Console, {
logger.add(new winston.transports.Console({
level: WIKI.config.logLevel,
prettyPrint: true,
colorize: true,
silent: false,
timestamp: true
})
}))
}
}
......@@ -10,12 +10,12 @@ module.exports = {
props: ['token', 'subdomain'],
init (logger, conf) {
require('winston-loggly-bulk')
logger.add(winston.transports.Loggly, {
logger.add(new winston.transports.Loggly({
token: conf.token,
subdomain: conf.subdomain,
tags: ['wiki-js'],
level: 'warn',
json: true
})
}))
}
}
......@@ -10,11 +10,11 @@ module.exports = {
props: ['host', 'port'],
init (logger, conf) {
require('winston-papertrail').Papertrail // eslint-disable-line no-unused-expressions
logger.add(winston.transports.Papertrail, {
logger.add(new winston.transports.Papertrail({
host: conf.host,
port: conf.port,
level: 'warn',
program: 'wiki.js'
})
}))
}
}
......@@ -24,9 +24,9 @@ module.exports = {
callback(null, true)
}
logger.add(RollbarLogger, {
logger.add(new RollbarLogger({
level: 'warn',
key: conf.key
})
}))
}
}
......@@ -24,9 +24,9 @@ module.exports = {
callback(null, true)
}
logger.add(SentryLogger, {
logger.add(new SentryLogger({
level: 'warn',
key: conf.key
})
}))
}
}
......@@ -276,7 +276,6 @@ module.exports = () => {
// Populate config namespaces
WIKI.config.auth = WIKI.config.auth || {}
WIKI.config.features = WIKI.config.features || {}
WIKI.config.git = WIKI.config.git || {}
WIKI.config.logging = WIKI.config.logging || {}
WIKI.config.site = WIKI.config.site || {}
WIKI.config.theme = WIKI.config.theme || {}
......@@ -290,7 +289,7 @@ module.exports = () => {
// Auth namespace
_.set(WIKI.config.auth, 'public', req.body.public === 'true')
_.set(WIKI.config.auth, 'strategies.local.enabled', true)
_.set(WIKI.config.auth, 'strategies.local.isEnabled', true)
_.set(WIKI.config.auth, 'strategies.local.allowSelfRegister', req.body.selfRegister === 'true')
// Logging namespace
......
......@@ -3,8 +3,9 @@ extends ../master.pug
block body
body
#app.is-fullscreen
.onboarding
img(src='/svg/logo-wikijs.svg', alt='Wiki.js')
h1= t('welcome.title')
h2= t('welcome.subtitle')
a.button.is-blue(href='/e/home')= t('welcome.createhome')
v-app
.onboarding
img(src='/svg/logo-wikijs.svg', alt='Wiki.js')
h1= t('welcome.title')
h2= t('welcome.subtitle')
v-btn(color='primary', href='/e/home')= t('welcome.createhome')
......@@ -4,7 +4,7 @@ const Promise = require('bluebird')
module.exports = Promise.join(
WIKI.db.onReady,
WIKI.configSvc.loadFromDb(['features', 'git', 'logging', 'site', 'uploads'])
WIKI.configSvc.loadFromDb(['features', 'logging', 'site', 'uploads'])
).then(() => {
const path = require('path')
......@@ -25,7 +25,7 @@ module.exports = Promise.join(
const i18nBackend = require('i18next-node-fs-backend')
WIKI.lang.use(i18nBackend).init({
load: 'languageOnly',
ns: ['common', 'admin', 'auth', 'errors', 'git'],
ns: ['common', 'admin', 'auth', 'errors'],
defaultNS: 'common',
saveMissing: false,
preload: [WIKI.config.lang],
......
This diff was suppressed by a .gitattributes entry.
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