Browse Source

feat: admin auth UI + fetch

pull/621/head
NGPixel 6 years ago
parent
commit
5efbfc7370
10 changed files with 2459 additions and 3617 deletions
  1. 169
      client/components/admin/admin-auth.vue
  2. 1
      client/components/admin/admin-system.vue
  3. 27
      client/components/login.vue
  4. 12
      client/graph/admin-auth-mutation-save-strategies.gql
  5. 15
      client/graph/admin-auth-query-strategies.gql
  6. 87
      client/static/svg/henry-reading.svg
  7. 90
      package.json
  8. 4
      server/graph/resolvers/authentication.js
  9. 8
      server/graph/schemas/authentication.graphql
  10. 5663
      yarn.lock

169
client/components/admin/admin-auth.vue

@ -1,112 +1,103 @@
<template lang='pug'> <template lang='pug'>
v-card(flat)
v-card(color='grey lighten-5')
.pa-3.pt-4
.headline.primary--text Authentication
.subheading.grey--text Configure the authentication settings of your wiki
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 activeProviders', :key='provider.key') {{ provider.title }}
v-card(tile, color='grey lighten-5')
.pa-3.pt-4
.headline.primary--text Authentication
.subheading.grey--text Configure the authentication settings of your wiki
v-tabs(color='grey lighten-4', fixed-tabs, slider-color='primary', show-arrows)
v-tab(key='settings'): v-icon settings
v-tab(v-for='strategy in activeStrategies', :key='strategy.key') {{ strategy.title }}
v-tab-item(key='settings', :transition='false', :reverse-transition='false')
v-card.pa-3
.body-2.pb-2 Select which authentication providers to enable:
v-form
v-checkbox(
v-for='(provider, n) in providers',
v-model='auths',
:key='provider.key',
:label='provider.title',
:value='provider.key',
color='primary',
:disabled='provider.key === `local`'
hide-details
)
v-divider
v-btn(color='primary')
v-icon(left) chevron_right
| Set Providers
v-btn(color='black', dark)
v-icon(left) layers_clear
| Flush Sessions
v-btn(icon, @click='refresh')
v-icon.grey--text refresh
v-tab-item(key='settings', :transition='false', :reverse-transition='false')
v-card.pa-3(flat, tile)
v-subheader.pl-0.pb-2 Select which authentication strategies to enable:
v-form
v-checkbox(
v-for='strategy in strategies',
v-model='selectedStrategies',
:key='strategy.key',
:label='strategy.title',
:value='strategy.key',
color='primary',
:disabled='strategy.key === `local`'
hide-details
)
v-tab-item(v-for='(provider, n) in activeProviders', :key='provider.key', :transition='false', :reverse-transition='false')
v-card.pa-3
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
| Save Configuration
v-tab-item(v-for='(strategy, n) in activeStrategies', :key='strategy.key', :transition='false', :reverse-transition='false')
v-card.pa-3(flat, tile)
v-form
v-subheader.pl-0 Strategy Configuration
.body-1.ml-3(v-if='!strategy.config || strategy.config.length < 1') This strategy has no configuration options you can modify.
v-text-field(v-else, v-for='cfg in strategy.config', :key='cfg.key', :label='cfg.key', prepend-icon='settings_applications')
v-divider
v-subheader.pl-0 Registration
v-switch.ml-3(
v-model='auths',
label='Allow self-registration',
:value='true',
color='primary',
hint='Allow any user successfully authorized by the strategy to access the wiki.',
persistent-hint
)
v-text-field.ml-3(label='Limit to specific email domains', prepend-icon='mail_outline')
v-text-field.ml-3(label='Assign to group', prepend-icon='people')
v-divider.my-0
v-card-actions.grey.lighten-4
v-btn(color='primary')
v-icon(left) chevron_right
span Save
v-spacer
v-btn(icon, @click='refresh')
v-icon.grey--text refresh
v-snackbar(
color='success'
top
v-model='refreshCompleted'
)
v-icon.mr-3(dark) cached
| List of providers has been refreshed.
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import gql from 'graphql-tag'
import strategiesQuery from 'gql/admin-auth-query-strategies.gql'
import strategiesSaveMutation from 'gql/admin-auth-mutation-save-strategies.gql'
export default { export default {
data() { data() {
return { return {
providers: [],
auths: ['local'],
refreshCompleted: false
strategies: [],
selectedStrategies: ['local']
} }
}, },
computed: { computed: {
activeProviders() {
return _.filter(this.providers, 'isEnabled')
activeStrategies() {
return _.filter(this.strategies, prv => _.includes(this.selectedStrategies, prv.key))
} }
}, },
apollo: {
providers: {
query: gql`
query {
authentication {
providers {
isEnabled
key
props
title
useForm
config {
key
value
}
}
}
methods: {
async refresh() {
await this.$apollo.queries.strategies.refetch()
this.$store.commit('showNotification', {
message: 'List of strategies has been refreshed.',
style: 'success',
icon: 'cached'
})
},
async saveProviders() {
this.$store.commit(`loadingStart`, 'admin-auth-savestrategies')
await this.$apollo.mutate({
mutation: strategiesSaveMutation,
variables: {
strategies: this.auths
} }
`,
update: (data) => data.authentication.providers
})
this.$store.commit(`loadingStop`, 'admin-auth-savestrategies')
} }
}, },
methods: {
async refresh() {
await this.$apollo.queries.providers.refetch()
this.refreshCompleted = true
apollo: {
strategies: {
query: strategiesQuery,
fetchPolicy: 'network-only',
update: (data) => data.authentication.strategies,
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-refresh')
}
} }
} }
} }

1
client/components/admin/admin-system.vue

@ -132,6 +132,7 @@ export default {
apollo: { apollo: {
info: { info: {
query: systemInfoQuery, query: systemInfoQuery,
fetchPolicy: 'network-only',
update: (data) => data.system.info, update: (data) => data.system.info,
watchLoading (isLoading) { watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-system-refresh') this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-system-refresh')

27
client/components/login.vue

@ -1,8 +1,10 @@
<template lang="pug"> <template lang="pug">
v-app v-app
nav-header nav-header
.login(:class='{ "is-error": error }')
.login
.login-container(:class='{ "is-expanded": strategies.length > 1, "is-loading": isLoading }') .login-container(:class='{ "is-expanded": strategies.length > 1, "is-loading": isLoading }')
.login-mascot
img(src='/svg/henry-reading.svg', alt='Henry')
.login-providers(v-show='strategies.length > 1') .login-providers(v-show='strategies.length > 1')
button(v-for='strategy in strategies', :class='{ "is-active": strategy.key === selectedStrategy }', @click='selectStrategy(strategy.key, strategy.useForm)', :title='strategy.title') button(v-for='strategy in strategies', :class='{ "is-active": strategy.key === selectedStrategy }', @click='selectStrategy(strategy.key, strategy.useForm)', :title='strategy.title')
em(v-html='strategy.icon') em(v-html='strategy.icon')
@ -160,7 +162,9 @@ export default {
style: 'success', style: 'success',
icon: 'check' icon: 'check'
}) })
window.location.replace('/') // TEMPORARY - USE RETURNURL
_.delay(() => {
window.location.replace('/') // TEMPORARY - USE RETURNURL
}, 1000)
} }
this.isLoading = false this.isLoading = false
} else { } else {
@ -205,6 +209,9 @@ export default {
style: 'success', style: 'success',
icon: 'check' icon: 'check'
}) })
_.delay(() => {
window.location.replace('/') // TEMPORARY - USE RETURNURL
}, 1000)
this.isLoading = false this.isLoading = false
} else { } else {
throw new Error(respObj.responseResult.message) throw new Error(respObj.responseResult.message)
@ -275,6 +282,20 @@ export default {
height: 25vh; height: 25vh;
} }
&-mascot {
width: 200px;
height: 200px;
position: absolute;
top: -180px;
left: 50%;
margin-left: -100px;
z-index: 10;
@include until($tablet) {
display: none;
}
}
&-container { &-container {
position: relative; position: relative;
display: flex; display: flex;
@ -453,7 +474,7 @@ export default {
font-weight: 400; font-weight: 400;
color: mc('light-blue', '700'); color: mc('light-blue', '700');
text-shadow: 1px 1px 0 #FFF; text-shadow: 1px 1px 0 #FFF;
padding: 0;
padding: 1rem 0 0 0;
margin: 0; margin: 0;
} }

12
client/graph/admin-auth-mutation-save-strategies.gql

@ -0,0 +1,12 @@
mutation($locale: String!, $autoUpdate: Boolean!, $namespacing: Boolean!, $namespaces: [String]!) {
localization {
updateLocale(locale: $locale, autoUpdate: $autoUpdate, namespacing: $namespacing, namespaces: $namespaces) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}

15
client/graph/admin-auth-query-strategies.gql

@ -0,0 +1,15 @@
query {
authentication {
strategies {
isEnabled
key
props
title
useForm
config {
key
value
}
}
}
}

87
client/static/svg/henry-reading.svg
File diff suppressed because it is too large
View File

90
package.json

@ -34,7 +34,7 @@
}, },
"homepage": "https://github.com/Requarks/wiki#readme", "homepage": "https://github.com/Requarks/wiki#readme",
"engines": { "engines": {
"node": ">=8.9.3"
"node": ">=8.11"
}, },
"dependencies": { "dependencies": {
"apollo-server-express": "1.3.6", "apollo-server-express": "1.3.6",
@ -43,7 +43,7 @@
"bcryptjs-then": "1.0.1", "bcryptjs-then": "1.0.1",
"bluebird": "3.5.1", "bluebird": "3.5.1",
"body-parser": "1.18.3", "body-parser": "1.18.3",
"bugsnag": "2.3.1",
"bugsnag": "2.4.0",
"bull": "3.4.2", "bull": "3.4.2",
"cheerio": "1.0.0-rc.2", "cheerio": "1.0.0-rc.2",
"child-process-promise": "2.2.1", "child-process-promise": "2.2.1",
@ -67,22 +67,22 @@
"getos": "3.1.0", "getos": "3.1.0",
"graphql": "0.13.2", "graphql": "0.13.2",
"graphql-list-fields": "2.0.2", "graphql-list-fields": "2.0.2",
"graphql-tools": "3.0.1",
"graphql-tools": "3.0.2",
"i18next": "11.3.2", "i18next": "11.3.2",
"i18next-express-middleware": "1.1.1", "i18next-express-middleware": "1.1.1",
"i18next-localstorage-cache": "1.1.1", "i18next-localstorage-cache": "1.1.1",
"i18next-node-fs-backend": "1.0.0", "i18next-node-fs-backend": "1.0.0",
"image-size": "0.6.2", "image-size": "0.6.2",
"ioredis": "3.2.2", "ioredis": "3.2.2",
"js-yaml": "3.11.0",
"jsonwebtoken": "8.2.1",
"js-yaml": "3.12.0",
"jsonwebtoken": "8.2.2",
"klaw": "2.1.1", "klaw": "2.1.1",
"knex": "0.14.6", "knex": "0.14.6",
"lodash": "4.17.10", "lodash": "4.17.10",
"markdown-it": "8.4.1", "markdown-it": "8.4.1",
"markdown-it-abbr": "1.0.4", "markdown-it-abbr": "1.0.4",
"markdown-it-anchor": "4.0.0", "markdown-it-anchor": "4.0.0",
"markdown-it-attrs": "2.0.0",
"markdown-it-attrs": "2.1.0",
"markdown-it-emoji": "1.4.0", "markdown-it-emoji": "1.4.0",
"markdown-it-expand-tabs": "1.0.13", "markdown-it-expand-tabs": "1.0.13",
"markdown-it-external-links": "0.0.6", "markdown-it-external-links": "0.0.6",
@ -95,7 +95,7 @@
"markdown-it-task-lists": "2.1.1", "markdown-it-task-lists": "2.1.1",
"mathjax-node": "2.1.0", "mathjax-node": "2.1.0",
"mime-types": "2.1.18", "mime-types": "2.1.18",
"moment": "2.22.1",
"moment": "2.22.2",
"moment-timezone": "0.5.17", "moment-timezone": "0.5.17",
"mongodb": "3.1.0-beta4", "mongodb": "3.1.0-beta4",
"mssql": "4.1.0", "mssql": "4.1.0",
@ -103,7 +103,7 @@
"mysql2": "1.5.3", "mysql2": "1.5.3",
"node-2fa": "1.1.2", "node-2fa": "1.1.2",
"oauth2orize": "1.11.0", "oauth2orize": "1.11.0",
"objection": "1.1.8",
"objection": "1.1.10",
"ora": "2.1.0", "ora": "2.1.0",
"passport": "0.4.0", "passport": "0.4.0",
"passport-auth0": "0.6.1", "passport-auth0": "0.6.1",
@ -126,8 +126,8 @@
"qr-image": "3.2.0", "qr-image": "3.2.0",
"raven": "2.6.2", "raven": "2.6.2",
"read-chunk": "2.1.0", "read-chunk": "2.1.0",
"remove-markdown": "0.2.2",
"request": "2.86.0",
"remove-markdown": "0.3.0",
"request": "2.87.0",
"request-promise": "4.2.2", "request-promise": "4.2.2",
"scim-query-filter-parser": "1.1.0", "scim-query-filter-parser": "1.1.0",
"semver": "5.5.0", "semver": "5.5.0",
@ -140,28 +140,31 @@
"yargs": "11.0.0" "yargs": "11.0.0"
}, },
"devDependencies": { "devDependencies": {
"@panter/vue-i18next": "0.9.1",
"@vue/cli": "3.0.0-beta.10",
"apollo-client-preset": "1.0.8",
"@panter/vue-i18next": "0.11.0",
"@vue/cli": "3.0.0-beta.15",
"apollo-cache-inmemory": "1.2.2",
"apollo-client": "2.3.2",
"apollo-fetch": "0.7.0", "apollo-fetch": "0.7.0",
"apollo-link": "1.2.2",
"apollo-link-batch-http": "1.2.2", "apollo-link-batch-http": "1.2.2",
"autoprefixer": "8.5.0",
"apollo-link-error": "1.0.9",
"apollo-link-http": "1.5.4",
"autoprefixer": "8.6.0",
"babel-cli": "6.26.0", "babel-cli": "6.26.0",
"babel-core": "6.26.3", "babel-core": "6.26.3",
"babel-eslint": "8.2.3", "babel-eslint": "8.2.3",
"babel-jest": "23.0.0-alpha.0",
"babel-jest": "23.0.1",
"babel-loader": "7.1.4", "babel-loader": "7.1.4",
"babel-plugin-graphql-tag": "1.6.0", "babel-plugin-graphql-tag": "1.6.0",
"babel-plugin-lodash": "3.3.2", "babel-plugin-lodash": "3.3.2",
"babel-plugin-transform-imports": "1.5.0", "babel-plugin-transform-imports": "1.5.0",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"babel-preset-env": "1.7.0", "babel-preset-env": "1.7.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-stage-2": "6.24.1", "babel-preset-stage-2": "6.24.1",
"brace": "0.11.1", "brace": "0.11.1",
"cache-loader": "1.2.2", "cache-loader": "1.2.2",
"clean-webpack-plugin": "0.1.19", "clean-webpack-plugin": "0.1.19",
"colors": "1.2.5",
"colors": "1.3.0",
"copy-webpack-plugin": "4.5.1", "copy-webpack-plugin": "4.5.1",
"css-loader": "0.28.11", "css-loader": "0.28.11",
"cssnano": "4.0.0-rc.2", "cssnano": "4.0.0-rc.2",
@ -171,7 +174,7 @@
"eslint-config-standard": "11.0.0", "eslint-config-standard": "11.0.0",
"eslint-plugin-import": "2.12.0", "eslint-plugin-import": "2.12.0",
"eslint-plugin-node": "6.0.1", "eslint-plugin-node": "6.0.1",
"eslint-plugin-promise": "3.7.0",
"eslint-plugin-promise": "3.8.0",
"eslint-plugin-standard": "3.1.0", "eslint-plugin-standard": "3.1.0",
"eslint-plugin-vue": "4.5.0", "eslint-plugin-vue": "4.5.0",
"file-loader": "1.1.11", "file-loader": "1.1.11",
@ -183,27 +186,28 @@
"html-webpack-pug-plugin": "0.3.0", "html-webpack-pug-plugin": "0.3.0",
"i18next-xhr-backend": "1.5.1", "i18next-xhr-backend": "1.5.1",
"ignore-loader": "0.1.2", "ignore-loader": "0.1.2",
"jest": "22.4.4",
"jest-junit": "4.0.0",
"jest": "23.1.0",
"jest-junit": "5.0.0",
"js-cookie": "2.2.0", "js-cookie": "2.2.0",
"lodash-webpack-plugin": "0.11.5", "lodash-webpack-plugin": "0.11.5",
"mini-css-extract-plugin": "0.4.0", "mini-css-extract-plugin": "0.4.0",
"node-sass": "4.9.0", "node-sass": "4.9.0",
"offline-plugin": "5.0.3",
"optimize-css-assets-webpack-plugin": "4.0.1",
"offline-plugin": "5.0.5",
"optimize-css-assets-webpack-plugin": "4.0.2",
"postcss-cssnext": "3.1.0", "postcss-cssnext": "3.1.0",
"postcss-flexbugs-fixes": "3.3.1", "postcss-flexbugs-fixes": "3.3.1",
"postcss-flexibility": "2.0.0", "postcss-flexibility": "2.0.0",
"postcss-import": "11.1.0", "postcss-import": "11.1.0",
"postcss-loader": "2.1.5", "postcss-loader": "2.1.5",
"postcss-preset-env": "5.1.0",
"postcss-selector-parser": "5.0.0-rc.3", "postcss-selector-parser": "5.0.0-rc.3",
"pug-lint": "2.5.0", "pug-lint": "2.5.0",
"pug-loader": "2.4.0", "pug-loader": "2.4.0",
"pug-plain-loader": "1.0.0", "pug-plain-loader": "1.0.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"react": "16.3.2",
"react-dom": "16.3.2",
"sass-loader": "7.0.1",
"react": "16.4.0",
"react-dom": "16.4.0",
"sass-loader": "7.0.2",
"sass-resources-loader": "1.3.3", "sass-resources-loader": "1.3.3",
"script-ext-html-webpack-plugin": "2.0.1", "script-ext-html-webpack-plugin": "2.0.1",
"simple-progress-webpack-plugin": "1.1.2", "simple-progress-webpack-plugin": "1.1.2",
@ -211,15 +215,16 @@
"stylus": "0.54.5", "stylus": "0.54.5",
"stylus-loader": "3.0.2", "stylus-loader": "3.0.2",
"twemoji-awesome": "1.0.6", "twemoji-awesome": "1.0.6",
"vee-validate": "2.0.9",
"url-loader": "1.0.1",
"vee-validate": "2.1.0-beta.1",
"velocity-animate": "1.5.1", "velocity-animate": "1.5.1",
"vue": "2.5.16", "vue": "2.5.16",
"vue-apollo": "3.0.0-beta.5",
"vue-apollo": "3.0.0-beta.16",
"vue-clipboards": "1.2.4", "vue-clipboards": "1.2.4",
"vue-codemirror": "4.0.5", "vue-codemirror": "4.0.5",
"vue-hot-reload-api": "2.3.0", "vue-hot-reload-api": "2.3.0",
"vue-loader": "15.1.0",
"vue-material-design-icons": "1.4.0",
"vue-loader": "15.2.4",
"vue-material-design-icons": "1.5.1",
"vue-moment": "4.0.0-0", "vue-moment": "4.0.0-0",
"vue-router": "3.0.1", "vue-router": "3.0.1",
"vue-simple-breakpoints": "1.0.3", "vue-simple-breakpoints": "1.0.3",
@ -227,9 +232,9 @@
"vuetify": "1.0.18", "vuetify": "1.0.18",
"vuex": "3.0.1", "vuex": "3.0.1",
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
"webpack": "4.8.3",
"webpack-bundle-analyzer": "2.12.0",
"webpack-cli": "2.1.3",
"webpack": "4.10.2",
"webpack-bundle-analyzer": "2.13.1",
"webpack-cli": "3.0.1",
"webpack-dev-middleware": "3.1.3", "webpack-dev-middleware": "3.1.3",
"webpack-hot-middleware": "2.22.2", "webpack-hot-middleware": "2.22.2",
"webpack-merge": "4.1.2", "webpack-merge": "4.1.2",
@ -271,7 +276,8 @@
] ]
}, },
"postcss-flexbugs-fixes": {}, "postcss-flexbugs-fixes": {},
"postcss-flexibility": {}
"postcss-flexibility": {},
"postcss-preset-env": {}
} }
}, },
"pugLintConfig": { "pugLintConfig": {
@ -290,24 +296,6 @@
"validateDivTags": true, "validateDivTags": true,
"validateIndentation": 2 "validateIndentation": 2
}, },
"nodemonConfig": {
"exec": "node server --dev",
"ignore": [
"assets/",
"client/",
"data/",
"dev/",
"test/",
"test-results/"
],
"ext": "js json graphql",
"watch": [
"server"
],
"env": {
"NODE_ENV": "development"
}
},
"collective": { "collective": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/wikijs", "url": "https://opencollective.com/wikijs",

4
server/graph/resolvers/authentication.js

@ -15,7 +15,7 @@ module.exports = {
async authentication() { return {} } async authentication() { return {} }
}, },
AuthenticationQuery: { AuthenticationQuery: {
async providers(obj, args, context, info) {
async strategies(obj, args, context, info) {
let strategies = await WIKI.db.authentication.query().orderBy('title') let strategies = await WIKI.db.authentication.query().orderBy('title')
strategies = strategies.map(stg => ({ strategies = strategies.map(stg => ({
...stg, ...stg,
@ -52,7 +52,7 @@ module.exports = {
} }
} }
}, },
AuthenticationProvider: {
AuthenticationStrategy: {
icon (ap, args) { icon (ap, args) {
return fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${ap.key}.svg`), 'utf8').catch(err => { return fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${ap.key}.svg`), 'utf8').catch(err => {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {

8
server/graph/schemas/authentication.graphql

@ -15,10 +15,10 @@ extend type Mutation {
# ----------------------------------------------- # -----------------------------------------------
type AuthenticationQuery { type AuthenticationQuery {
providers(
strategies(
filter: String filter: String
orderBy: String orderBy: String
): [AuthenticationProvider]
): [AuthenticationStrategy]
} }
# ----------------------------------------------- # -----------------------------------------------
@ -37,7 +37,7 @@ type AuthenticationMutation {
securityCode: String! securityCode: String!
): DefaultResponse ): DefaultResponse
updateProvider(
updateStrategy(
provider: String! provider: String!
isEnabled: Boolean! isEnabled: Boolean!
config: [KeyValuePairInput] config: [KeyValuePairInput]
@ -48,7 +48,7 @@ type AuthenticationMutation {
# TYPES # TYPES
# ----------------------------------------------- # -----------------------------------------------
type AuthenticationProvider {
type AuthenticationStrategy {
isEnabled: Boolean! isEnabled: Boolean!
key: String! key: String!
props: [String] props: [String]

5663
yarn.lock
File diff suppressed because it is too large
View File

Loading…
Cancel
Save