Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add storage tab #100

Merged
merged 19 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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