Skip to content

Commit

Permalink
feat: add storage tab (#100)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <[email protected]>
  • Loading branch information
atinux and antfu authored Feb 28, 2023
1 parent 579e091 commit c153313
Show file tree
Hide file tree
Showing 18 changed files with 336 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dist
.build-*
.env
.netlify
.data

# Env
.env
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools-ui-kit/playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const radio = ref('a')

<template>
<div class="relative p-10 n-bg-base">
<div class="w-full flex container mx-auto flex-col gap-4">
<div class="w-full flex gap-4 mx-auto flex-col container">
<NTip n="hover:yellow-600 dark:hover:yellow-500">
This library is heavily working in progress. Breaking changes may not follow semver. Pin the version if used.
</NTip>
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools-ui-kit/src/components/NDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ onClickOutside(el, () => {
</slot>

<div
class="absolute n-transition n-bg-base rounded z-10 border n-border-base shadow"
class="absolute n-transition n-bg-base rounded border z-10 n-border-base shadow"
:class="[enabled ? 'op-100' : 'op0 pointer-events-none -translate-y-1']"
>
<slot />
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export default defineBuildConfig({
// Type only
'vue',
'vue-router',
'unstorage',
'nitropack',
],
rollup: {
inlineDependencies: true,
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/client/app.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script setup lang="ts">
import 'floating-vue/dist/style.css'
import 'vanilla-jsoneditor/themes/jse-theme-dark.css'
import 'splitpanes/dist/splitpanes.css'
import './styles/global.css'
import { setupClientRPC } from './setup/client-rpc'
Expand Down
6 changes: 2 additions & 4 deletions packages/devtools/client/components/DrawerItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ const isEnabled = computed(() => {
<NuxtLink
v-if="isEnabled"
:to="'path' in tab ? tab.path : `/modules/custom-${tab.name}`"
flex="~" p2
items-center justify-center
text-true-gray
flex="~" p2 items-center justify-center text-secondary
border="base"
lg="border-b px3 py1.5 justify-start"
hover:bg-active
hover="bg-active"
select-none
exact-active-class="!text-primary"
>
Expand Down
1 change: 0 additions & 1 deletion packages/devtools/client/components/IframeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const props = defineProps<{
tab: ModuleCustomTab
}>()
const client = useClient()
const colorMode = useColorMode()
const anchor = ref<HTMLDivElement>()
const key = computed(() => props.tab.name)
Expand Down
4 changes: 4 additions & 0 deletions packages/devtools/client/components/SettingsPanel.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
const {
interactionCloseOnOutsideClick,
showExperimentalFeatures,
} = useDevToolsSettings()
</script>

Expand All @@ -9,5 +10,8 @@ const {
<NCheckbox v-model="interactionCloseOnOutsideClick" n-primary>
<span>Close DevTools when clicking outside</span>
</NCheckbox>
<NCheckbox v-model="showExperimentalFeatures" n-primary>
<span>Show experimental features</span>
</NCheckbox>
</div>
</template>
22 changes: 0 additions & 22 deletions packages/devtools/client/components/StateEditor.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
import JsonEditorVue from 'json-editor-vue'
import 'vanilla-jsoneditor/themes/jse-theme-dark.css'
const props = defineProps<{
name?: string
Expand Down Expand Up @@ -94,24 +93,3 @@ async function refresh() {
</template>
</div>
</template>

<style>
.json-editor-vue {
--jse-theme-color: #383e42 !important;
--jse-theme-color-highlight: #687177 !important;
--jse-background-color: #8881 !important;
}
.json-editor-vue .no-main-menu {
border: none !important;
}
.json-editor-vue .jse-main {
min-height: 1em !important;
}
.json-editor-vue .jse-contents {
border-width: 0 !important;
border-radius: 5px !important;
}
</style>
6 changes: 2 additions & 4 deletions packages/devtools/client/composables/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ export function useCustomTabs() {
export function useTabs() {
const router = useRouter()
const customTabs = useCustomTabs()
const settings = useDevToolsSettings()

const builtin = computed(() => {
return router.getRoutes()
.filter(route => route.path.startsWith('/modules/') && route.meta.title && !route.meta.wip)
.filter(route => !route.meta.experimental || (route.meta.experimental && settings.showExperimentalFeatures.value))
.sort((a, b) => (a.meta.order || 100) - (b.meta.order || 100))
.map((i): ModuleBuiltinTab => {
return {
Expand Down Expand Up @@ -131,7 +133,3 @@ export function useAllRoutes() {
})
})
}

function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
1 change: 1 addition & 0 deletions packages/devtools/client/composables/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const devToolsSettings = useLocalStorage<DevToolsUISettings>('nuxt-devtools-sett
componentsGraphShowLayouts: false,
componentsGraphShowWorkspace: true,
interactionCloseOnOutsideClick: false,
showExperimentalFeatures: false,
}, { mergeDefaults: true })

const devToolsSettingsRefs = toRefs(devToolsSettings)
Expand Down
9 changes: 6 additions & 3 deletions packages/devtools/client/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ export default defineNuxtConfig({
output: {
publicDir: resolve(__dirname, '../dist/client'),
},
devStorage: {
test: {
driver: 'fs',
base: resolve(__dirname, './.data/test'),
},
},
},
css: [
'splitpanes/dist/splitpanes.css',
],
appConfig: {
fixture2: 'from nuxt.config.ts',
},
Expand Down
219 changes: 219 additions & 0 deletions packages/devtools/client/pages/modules/storage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<script setup lang="ts">
import JsonEditorVue from 'json-editor-vue'
definePageMeta({
icon: 'carbon-data-base',
title: 'Storage',
experimental: true,
})
const nuxtApp = useNuxtApp()
const router = useRouter()
const searchString = ref('')
const newKey = ref('')
const currentStorage = computed({
get(): string | undefined {
return useRoute().query?.storage as string | undefined
},
set(storage: string | undefined): void {
router.replace({ query: { storage } })
},
})
const currentItem = ref()
const fileKey = computed(() => useRoute().query?.key as string | undefined)
const { data: storageMounts } = await useAsyncData('storageMounts', () => rpc.getStorageMounts())
const { data: storageKeys, refresh: refreshStorageKeys } = await useAsyncData('storageKeys', async () => {
if (currentStorage.value)
return await rpc.getStorageKeys(currentStorage.value)
return []
})
const closeWatcher = nuxtApp.hook('storage:key:update' as any, async (key: string, event: any) => {
if (!currentStorage.value || key.split(':')[0] !== currentStorage.value)
return
await refreshStorageKeys()
if (fileKey.value === key) {
if (event === 'remove')
return router.replace({ query: { storage: currentStorage.value } })
await fetchItem(fileKey.value)
}
})
onUnmounted(closeWatcher)
watch(currentStorage, refreshStorageKeys)
watchEffect(async () => {
if (!fileKey.value) {
currentItem.value = null
return
}
fetchItem(fileKey.value)
})
// Save on Ctrl/Cmd + S
useEventListener('keydown', (e) => {
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
saveCurrentItem()
e.preventDefault()
}
})
function keyName(key: string) {
return key.replace(`${currentStorage.value}:`, '')
}
const filteredKeys = computed(() => {
if (!storageKeys.value)
return []
return storageKeys.value.filter(key => key.includes(searchString.value))
})
async function fetchItem(key: string) {
const content = await rpc.getStorageItem(key)
currentItem.value = {
key,
updatedKey: keyName(key),
editingKey: false,
content,
updatedContent: content,
}
}
async function saveNewItem() {
if (!newKey.value || !currentStorage.value)
return
// If does not exists
const key = `${currentStorage.value}:${newKey.value}`
if (!storageKeys.value?.includes(key))
await rpc.setStorageItem(key, '')
router.replace({ query: { storage: currentStorage.value, key } })
newKey.value = ''
}
async function saveCurrentItem() {
if (!currentItem.value)
return
await rpc.setStorageItem(currentItem.value.key, currentItem.value.updatedContent)
await fetchItem(currentItem.value.key)
}
async function removeCurrentItem() {
if (!currentItem.value || !currentStorage.value)
return
await rpc.removeStorageItem(currentItem.value.key)
currentItem.value = null
}
async function renameCurrentItem() {
if (!currentItem.value || !currentStorage.value)
return
const renamedKey = `${currentStorage.value}:${currentItem.value.updatedKey}`
await rpc.setStorageItem(renamedKey, currentItem.value.updatedContent)
await rpc.removeStorageItem(currentItem.value.key)
router.replace({ query: { storage: currentStorage.value, key: renamedKey } })
}
</script>

<template>
<div v-if="currentStorage" grid="~ cols-[auto_1fr]" h-full of-hidden class="virtual-files">
<div border="r base" of-auto w="300px">
<div class="flex items-center justify-between px-3 h-[48px] gap1">
<button n-icon-btn ml--1 @click="currentStorage = ''">
<div i-carbon-chevron-left />
</button>
<div class="w-full text-sm">
<NSelect v-model="currentStorage" n="primary" icon="carbon-data-base">
<option v-for="(_storage, name) of storageMounts" :key="name" :value="name">
{{ name }}
</option>
</NSelect>
</div>
</div>
<NTextInput
v-model="searchString"
icon="carbon-search"
placeholder="Search..."
n="primary sm"
border="y x-none base! rounded-0"
class="w-full py2 ring-0!"
/>
<NuxtLink
v-for="key of filteredKeys" :key="key"
border="b base" px2 py1 text-sm font-mono block truncate
:to="{ query: { key, storage: currentStorage } }"
hover:bg-active
:class="key === currentItem?.key ? 'bg-active text-primary font-bold' : 'text-secondary'"
>
{{ keyName(key) }}
</NuxtLink>
<NTextInput
v-model="newKey"
icon="carbon-add"
placeholder="key"
n="sm"
border="t-none x-none base! rounded-0"
class="w-full py2 ring-0!"
@keyup.enter="saveNewItem"
/>
</div>
<div v-if="currentItem?.key" h-full of-hidden flex="~ col">
<div border="b base" class="text-sm flex items-center px-4 justify-between flex-none h-[49px]">
<div class="flex items-center gap-4">
<NTextInput v-if="currentItem.editingKey" v-model="currentItem.updatedKey" @keyup.enter="renameCurrentItem" />
<code v-else>{{ keyName(currentItem.key) }} <NIcon icon="carbon-edit" class="op50 hover:op100 cursor-pointer" @click="currentItem.editingKey = true" /></code>
<NButton v-if="!currentItem.editingKey" n="green xs" :disabled="currentItem.content === currentItem.updatedContent" :class="{ 'border-green': currentItem.content !== currentItem.updatedContent }" @click="saveCurrentItem">
Save
</NButton>
</div>
<div>
<NButton n="red xs" @click="removeCurrentItem">
Delete
</NButton>
</div>
</div>
<JsonEditorVue
v-if="typeof currentItem.content === 'object'"
v-model="currentItem.updatedContent"
:class="[$colorMode.value === 'dark' ? 'jse-theme-dark' : 'light']"
class="json-editor-vue of-auto h-full text-sm outline-none"
v-bind="$attrs" mode="text" :navigation-bar="false" :indentation="2" :tab-size="2"
/>
<textarea
v-else v-model="currentItem.updatedContent"
placeholder="Item value..."
class="of-auto h-full text-sm outline-none p-4 font-mono"
@keyup.ctrl.enter="saveCurrentItem"
/>
</div>
<div v-else flex items-center justify-center op50 text-center>
<p>
Select one key to start.<br>Learn more about <NLink href="https://nitro.unjs.io/guide/introduction/storage" n="orange" target="_blank">
Nitro storage
</NLink>
</p>
</div>
</div>
<div v-else grid="~" class="h-full of-hidden">
<div class="flex gap-4 justify-center op50 text-center flex-col">
<p v-if="Object.keys(storageMounts as any).length">
Select one storage to start:
</p>
<p v-else>
No custom storage defined in <code>nitro.storage</code>.<br>
Learn more about <NLink href="https://nitro.unjs.io/guide/introduction/storage" n="orange" target="_blank">
Nitro storage
</NLink>
</p>
<div class="mx-auto">
<NCard v-for="(storage, name) of storageMounts" :key="name" class="text-left p-4 cursor-pointer border mb-4 hover:border-green" @click="currentStorage = name">
<span class="font-bold">{{ name }}</span><br>
<span class="text-sm">{{ storage.driver }} driver</span><br>
<span v-if="storage.base" class="text-xs font-mono">{{ storage.base }}</span>
</NCard>
</div>
</div>
</div>
</template>
3 changes: 3 additions & 0 deletions packages/devtools/client/setup/client-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ export function setupClientRPC() {
// refresh useAsyncData
nuxt.hooks.callHookParallel('app:data:refresh', [type])
},
async callHook(hook: string, ...args: any[]) {
nuxt.hooks.callHookParallel(hook as any, ...args)
},
} satisfies ClientFunctions)
}
Loading

0 comments on commit c153313

Please sign in to comment.