feat(ui): install/uninstall plugin

This commit is contained in:
Guillaume Chau
2018-03-12 13:57:00 +01:00
parent da0d37ece4
commit 63ccde84e9
19 changed files with 331 additions and 35 deletions

View File

@@ -1,6 +1,7 @@
<template>
<div class="instant-search-input">
<VueInput
ref="input"
icon-left="search"
v-model="query"
class="big"
@@ -55,6 +56,10 @@ export default {
}
this.searchStore.start()
this.searchStore.refresh()
},
focus () {
this.$refs.input.focus()
}
}
}

View File

@@ -4,6 +4,7 @@
:class="{
selected,
loaded,
error,
vuejs: image && image.includes('vuejs')
}"
>
@@ -13,11 +14,12 @@
icon="done"
/>
<img
v-else-if="image"
v-else-if="image && !error"
class="image"
:src="image"
:key="image"
@load="loaded = true"
@error="error = true"
>
<VueIcon
v-else
@@ -48,13 +50,20 @@ export default {
data () {
return {
loaded: false
loaded: false,
error: false
}
},
watch: {
image (value) {
image: 'reset',
selected: 'reset'
},
methods: {
reset () {
this.loaded = false
this.error = false
}
}
}
@@ -97,10 +106,14 @@ export default {
animation zoom .1s
transform none
&.selected,
&.error
.wrapper
animation zoom .1s
&.selected
.wrapper
background $vue-color-primary
animation zoom .1s
.vue-icon
>>> svg
fill $vue-color-light

View File

@@ -31,6 +31,10 @@
attribute-name="description"
/>
</span>
<span v-if="official" class="info">
<VueIcon icon="star" class="top medium"/>
<span>Official</span>
</span>
<span class="info downloads">
<VueIcon class="medium" icon="file_download"/>
<span>{{ pkg.humanDownloadsLast30Days }}</span>
@@ -56,6 +60,12 @@ export default {
type: Boolean,
default: false
}
},
computed: {
official () {
return this.pkg.owner.name === 'vuejs'
}
}
}
</script>

View File

@@ -1,5 +1,6 @@
module.exports = {
CWD_CHANGED: 'cwd_changed',
PROGRESS_CHANGED: 'progress_changed',
PROGRESS_REMOVED: 'progress_removed',
CONSOLE_LOG_ADDED: 'console_log_added'
}

View File

@@ -1,11 +1,24 @@
const path = require('path')
const fs = require('fs')
const LRU = require('lru-cache')
const { isPlugin, isOfficialPlugin, getPluginLink } = require('@vue/cli-shared-utils')
const {
isPlugin,
isOfficialPlugin,
getPluginLink,
hasYarn
} = require('@vue/cli-shared-utils')
const getPackageVersion = require('@vue/cli/lib/util/getPackageVersion')
const {
progress: installProgress,
installPackage,
uninstallPackage
} = require('@vue/cli/lib/util/installDeps')
const { loadOptions } = require('@vue/cli/lib/options')
const cwd = require('./cwd')
const folders = require('./folders')
const prompts = require('./prompts')
const progress = require('./progress')
const metadataCache = new LRU({
max: 200,
@@ -16,6 +29,11 @@ const logoCache = new LRU({
max: 50
})
const PROGRESS_ID = 'plugin-installation'
let currentPluginId
let eventsInstalled = false
function getPath (id) {
return path.join(cwd.get(), 'node_modules', id)
}
@@ -108,9 +126,82 @@ async function getLogo ({ id }, context) {
return null
}
function getInstallation (context) {
if (!eventsInstalled) {
eventsInstalled = true
// Package installation progress events
installProgress.on('progress', value => {
if (progress.get(PROGRESS_ID)) {
progress.set({ id: PROGRESS_ID, progress: value }, context)
}
})
installProgress.on('log', message => {
if (progress.get(PROGRESS_ID)) {
progress.set({ id: PROGRESS_ID, info: message }, context)
}
})
}
return {
id: 'plugin-install',
pluginId: currentPluginId,
prompts: prompts.list()
}
}
function install (id, context) {
return progress.wrap(PROGRESS_ID, context, async setProgress => {
setProgress({
status: 'plugin-install',
args: [id]
})
currentPluginId = id
const packageManager = loadOptions().packageManager || (hasYarn() ? 'yarn' : 'npm')
await installPackage(cwd.get(), packageManager, null, id)
return getInstallation(context)
})
}
function uninstall (id, context) {
return progress.wrap(PROGRESS_ID, context, async setProgress => {
setProgress({
status: 'plugin-uninstall',
args: [id]
})
currentPluginId = id
const packageManager = loadOptions().packageManager || (hasYarn() ? 'yarn' : 'npm')
await uninstallPackage(cwd.get(), packageManager, null, id)
return getInstallation(context)
})
}
function invoke (id, context) {
return progress.wrap(PROGRESS_ID, context, async setProgress => {
setProgress({
status: 'plugin-invoke',
args: [id]
})
currentPluginId = id
// TODO
return getInstallation(context)
})
}
module.exports = {
list,
getVersion,
getDescription,
getLogo
getLogo,
getInstallation,
install,
uninstall,
invoke
}

View File

@@ -15,6 +15,7 @@ function set (data, context) {
status: null,
error: null,
info: null,
args: null,
progress: -1
}, progress))
} else {
@@ -25,6 +26,7 @@ function set (data, context) {
}
function remove (id, context) {
context.pubsub.publish(channels.PROGRESS_REMOVED, { progressRemoved: { id } })
return map.delete(id)
}

View File

@@ -42,7 +42,8 @@ module.exports = {
foldersFavorite: (root, args, context) => folders.listFavorite(context),
projects: (root, args, context) => projects.list(context),
projectCurrent: (root, args, context) => projects.getCurrent(context),
projectCreation: (root, args, context) => projects.getCreation(context)
projectCreation: (root, args, context) => projects.getCreation(context),
pluginInstallation: (root, args, context) => plugins.getInstallation(context)
},
Mutation: {
@@ -60,7 +61,10 @@ module.exports = {
projectImport: (root, { input }, context) => projects.import(input, context),
projectOpen: (root, { id }, context) => projects.open(id, context),
projectRemove: (root, { id }, context) => projects.remove(id, context),
projectCwdReset: (root, args, context) => projects.resetCwd(context)
projectCwdReset: (root, args, context) => projects.resetCwd(context),
pluginInstall: (root, { id }, context) => plugins.install(id, context),
pluginUninstall: (root, { id }, context) => plugins.uninstall(id, context),
pluginInvoke: (root, { id }, context) => plugins.invoke(id, context)
},
Subscription: {
@@ -75,6 +79,14 @@ module.exports = {
(payload, variables) => payload.progressChanged.id === variables.id
)
},
progressRemoved: {
subscribe: withFilter(
// Iterator
(parent, args, { pubsub }) => pubsub.asyncIterator(channels.PROGRESS_REMOVED),
// Filter
(payload, variables) => payload.progressRemoved.id === variables.id
)
},
consoleLogAdded: {
subscribe: (parent, args, context) => {
logs.init(context)

View File

@@ -84,10 +84,15 @@ type Plugin {
website: String
description: String
githubStats: GitHubStats
prompts: [Prompt]
logo: String
}
type PluginInstallation {
id: ID!
pluginId: ID
prompts: [Prompt]
}
type Feature {
id: ID!
name: String!
@@ -145,6 +150,7 @@ type Progress {
error: String
# Progress from 0 to 1 (-1 means disabled)
progress: Float
args: [String]
}
type Query {
@@ -157,6 +163,7 @@ type Query {
projects: [Project]
projectCurrent: Project
projectCreation: ProjectCreation
pluginInstallation: PluginInstallation
}
type Mutation {
@@ -172,12 +179,15 @@ type Mutation {
projectCwdReset: String
presetApply (id: ID!): ProjectCreation
featureSetEnabled (id: ID!, enabled: Boolean): Feature
pluginAdd (id: ID!): Plugin
promptAnswer (input: PromptInput!): [Prompt]
pluginInstall (id: ID!): PluginInstallation
pluginUninstall (id: ID!): PluginInstallation
pluginInvoke (id: ID!): PluginInstallation
}
type Subscription {
progressChanged (id: ID!): Progress
progressRemoved (id: ID!): ID
consoleLogAdded: ConsoleLog!
cwdChanged: String!
}

View File

@@ -0,0 +1,7 @@
#import "./pluginInstallationFragment.gql"
mutation pluginInstall ($id: ID!) {
pluginInstall (id: $id) {
...pluginInstallation
}
}

View File

@@ -0,0 +1,7 @@
#import "./pluginInstallationFragment.gql"
query pluginInstallation {
pluginInstallation {
...pluginInstallation
}
}

View File

@@ -0,0 +1,8 @@
#import "./promptFragment.gql"
fragment pluginInstallation on PluginInstallation {
id
prompts {
...prompt
}
}

View File

@@ -0,0 +1,7 @@
#import "./pluginInstallationFragment.gql"
mutation pluginInvoke ($id: ID!) {
pluginInvoke (id: $id) {
...pluginInstallation
}
}

View File

@@ -0,0 +1,7 @@
#import "./pluginInstallationFragment.gql"
mutation pluginUninstall ($id: ID!) {
pluginUninstall (id: $id) {
...pluginInstallation
}
}

View File

@@ -4,4 +4,5 @@ fragment progress on Progress {
info
error
progress
args
}

View File

@@ -0,0 +1,3 @@
subscription progressRemoved ($id: ID!) {
progressRemoved (id: $id)
}

View File

@@ -1,5 +1,6 @@
import PROGRESS from '../graphql/progress.gql'
import PROGRESS_CHANGED from '../graphql/progressChanged.gql'
import PROGRESS_REMOVED from '../graphql/progressRemoved.gql'
const messages = {
'creating': 'Creating project...',
@@ -9,7 +10,10 @@ const messages = {
'deps-install': 'Installing additional dependencies...',
'completion-hooks': 'Running completion hooks...',
'fetch-remote-preset': `Fetching remote preset...`,
'done': 'Successfully created project'
'done': 'Successfully created project',
'plugin-install': 'Installing {{arg0}}',
'plugin-uninstall': 'Uninstalling {{arg0}}',
'plugin-invoke': 'Invoking {{arg0}}'
}
// @vue/component
@@ -36,19 +40,34 @@ export default {
}
},
fetchPolicy: 'network-only',
subscribeToMore: {
document: PROGRESS_CHANGED,
variables () {
return {
id: this.progressId
subscribeToMore: [
{
document: PROGRESS_CHANGED,
variables () {
return {
id: this.progressId
}
},
updateQuery: (previousResult, { subscriptionData }) => {
return {
progress: subscriptionData.data.progressChanged
}
}
},
updateQuery: (previousResult, { subscriptionData }) => {
return {
progress: subscriptionData.data.progressChanged
{
document: PROGRESS_REMOVED,
variables () {
return {
id: this.progressId
}
},
updateQuery: () => {
return {
progress: null
}
}
}
}
]
}
},
@@ -61,7 +80,15 @@ export default {
if (!this.progress) return null
const { status } = this.progress
const message = messages[status]
let message = messages[status]
if (message && this.progress.args) {
for (let i = 0, l = this.progress.args.length; i < l; i++) {
message = message.replace(
new RegExp(`{{arg${i}}}`, 'g'),
this.progress.args[i]
)
}
}
return message || status || ''
}
}

View File

@@ -69,6 +69,9 @@ ansi-colors('white', $vue-color-light)
overflow-y auto
margin $padding-item 0
.ais-no-results
margin-top 42px
.ais-highlight
em
font-style normal

View File

@@ -32,10 +32,11 @@
attributesToHighlight: [
'name',
'description'
]
],
// filters: `keywords:vue-cli-plugin`
}"
>
<InstantSearchInput/>
<InstantSearchInput ref="searchInput"/>
<ais-results>
<PackageSearchItem
slot-scope="{ result }"
@@ -44,6 +45,12 @@
@click.native="selectedId = result.name"
/>
</ais-results>
<ais-no-results>
<div class="vue-empty">
<VueIcon icon="search" class="huge"/>
<div>No results found</div>
</div>
</ais-no-results>
<InstantSearchPagination/>
</ais-index>
</div>
@@ -61,7 +68,7 @@
</div>
<VueButton
icon-left="add"
icon-left="file_download"
:label="`Install ${selectedId || 'plugin'}`"
class="big primary"
:disabled="!selectedId"
@@ -136,10 +143,19 @@
/>
</div>
</VueModal>
<ProgressScreen
progress-id="plugin-installation"
/>
</div>
</template>
<script>
import PLUGIN_INSTALLATION from '../graphql/pluginInstallation.gql'
import PLUGIN_INSTALL from '../graphql/pluginInstall.gql'
import PLUGIN_UNINSTALL from '../graphql/pluginUninstall.gql'
import PROMPT_ANSWER from '../graphql/promptAnswer.gql'
export default {
name: 'ProjectPluginsAdd',
@@ -148,7 +164,15 @@ export default {
tabId: 'search',
selectedId: null,
enabledPrompts: [],
showCancelInstall: false
showCancelInstall: false,
pluginInstallation: null
}
},
apollo: {
pluginInstallation: {
query: PLUGIN_INSTALLATION,
fetchPolicy: 'netork-only'
}
},
@@ -158,14 +182,29 @@ export default {
}
},
mounted () {
requestAnimationFrame(() => {
this.$refs.searchInput.focus()
})
},
methods: {
cancel () {
this.$router.push({ name: 'project-home' })
},
installPlugin () {
// TODO
this.tabId = 'config'
async installPlugin () {
try {
await this.$apollo.mutate({
mutation: PLUGIN_INSTALL,
variables: {
id: this.selectedId
}
})
this.tabId = 'config'
} catch(e) {
console.error(e)
}
},
cancelInstall () {
@@ -174,18 +213,41 @@ export default {
this.showCancelInstall = false
},
uninstallPlugin () {
// TODO
this.cancelInstall()
async uninstallPlugin () {
this.showCancelInstall = false
try {
await this.$apollo.mutate({
mutation: PLUGIN_UNINSTALL,
variables: {
id: this.selectedId
}
})
this.cancelInstall()
} catch(e) {
console.error(e)
}
},
invokePlugin () {
async invokePlugin () {
// TODO
},
answerPrompt () {
// TODO
}
async answerPrompt ({ prompt, value }) {
await this.$apollo.mutate({
mutation: PROMPT_ANSWER,
variables: {
input: {
id: prompt.id,
value: JSON.stringify(value)
}
},
update: (store, { data: { promptAnswer } }) => {
const data = store.readQuery({ query: PLUGIN_INSTALLATION })
data.pluginInstallation.prompts = promptAnswer
store.writeQuery({ query: PLUGIN_INSTALLATION, data })
}
})
},
}
}
</script>

View File

@@ -281,3 +281,23 @@ exports.installPackage = async function (targetDir, command, cliRegistry, packag
await executeCommand(command, args, targetDir)
}
exports.uninstallPackage = async function (targetDir, command, cliRegistry, packageName) {
const args = []
if (command === 'npm') {
args.push('uninstall', '--loglevel', 'error')
} else if (command === 'yarn') {
args.push('remove')
} else {
throw new Error(`Unknown package manager: ${command}`)
}
await addRegistryToArgs(command, args, cliRegistry)
args.push(packageName)
debug(`command: `, command)
debug(`args: `, args)
await executeCommand(command, args, targetDir)
}