Skip to content

Commit

Permalink
chore: add XCopy
Browse files Browse the repository at this point in the history
Signed-off-by: John Cowen <john.cowen@konghq.com>
  • Loading branch information
johncowen committed May 3, 2024
1 parent 6f8fed6 commit c1b1a8e
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 119 deletions.

This file was deleted.

This file was deleted.

25 changes: 4 additions & 21 deletions src/app/common/TextWithCopyButton.vue
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
<template>
<div class="copy-button-wrapper">
<span class="text"><slot>{{ props.text }}</slot></span>

<KCopy
format="hidden"
:text="props.text"
/>
</div>
<XCopy
format="custom"
:text="props.text"
/>
</template>

<script lang="ts" setup>
const props = defineProps<{
text: string
}>()
</script>

<style lang="scss" scoped>
.copy-button-wrapper {
display: inline-flex;
align-items: center;
gap: $kui-space-40;
}
.text {
min-width: 0;
word-wrap: break-word;
}
</style>
86 changes: 28 additions & 58 deletions src/app/common/code-block/ResourceCodeBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,50 +13,44 @@
@reg-exp-mode-change="emit('reg-exp-mode-change', $event)"
>
<template #secondary-actions>
<KCodeBlockIconButton
:copy-tooltip="t('common.copyKubernetesText')"
theme="dark"
@click="async () => {
if (!isCopying) {
isCopying = true
// Trigger an update of KCopy’s text
text = toYamlRepresentation(await fetcher())
// Wait a Vue tick for KCopy to have received the new text
await nextTick()
// Copy the text
kCopyElement?.copy()
}
}"
<XDisclosure
v-slot="{ expanded, toggle }"
>
<KCopy
ref="kCopyElement"
<KCodeBlockIconButton
:copy-tooltip="t('common.copyKubernetesText')"
theme="dark"
@click="() => {
if (!expanded) {
toggle()
}
}"
>
<XIcon name="copy" />{{ t('common.copyKubernetesShortText') }}
</KCodeBlockIconButton>
<XCopy
v-slot="{ copy }"
format="hidden"
:text="text"
/>
{{ t('common.copyKubernetesShortText') }}
</KCodeBlockIconButton>
>
<slot
:copy="(cb: CopyCallback) => {
if (expanded) {
toggle()
}
cb((text: Entity) => copy(toYamlRepresentation(text)), (e: unknown) => console.error(e))
}"
:copying="expanded"
/>
</XCopy>
</XDisclosure>
</template>
</CodeBlock>
<slot
:copy="(cb: CopyCallback) => {
if (isCopying) {
isCopying = false
}
copy(cb)
}"
:copying="isCopying"
/>
</div>
</template>

<script lang="ts" setup>
import { KCopy } from '@kong/kongponents'
import { computed, nextTick, ref } from 'vue'
import { computed } from 'vue'
import CodeBlock from './CodeBlock.vue'
import type { SingleResourceParameters } from '@/types/api.d'
import type { Entity } from '@/types/index.d'
import { useI18n } from '@/utilities'
import { toYaml } from '@/utilities/toYaml'
Expand Down Expand Up @@ -87,33 +81,9 @@ const emit = defineEmits<{
(event: 'reg-exp-mode-change', isRegExpMode: boolean): void
}>()
const isCopying = ref(false)
const text = ref('')
const kCopyElement = ref<InstanceType<typeof KCopy> | null>(null)
const yamlUniversal = computed(() => toYamlRepresentation(props.resource))
// False negative. We should enable the TypeScript-specific linter rule, but that requires further changes.
// eslint-disable-next-line no-extra-parens
const copy = ref<(cb: CopyCallback) => void>(() => {})
let promise = new Promise((resolve: Resolve, reject) => {
copy.value = (cb) => cb(resolve, reject)
})
const fetcher = async (_params?: SingleResourceParameters): Promise<Entity> => {
try {
// yes, this is one of those places where `return await` is important
return await promise
} finally {
promise = new Promise((resolve, reject) => {
copy.value = (cb) => cb(resolve, reject)
})
}
}
function toYamlRepresentation(resource: Entity): string {
const { creationTime, modificationTime, ...resourceWithoutTimes } = resource
return toYaml(resourceWithoutTimes)
}
</script>
7 changes: 4 additions & 3 deletions src/app/meshes/views/MeshDetailTabsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
<AppView>
<template #title>
<h1>
<TextWithCopyButton :text="route.params.mesh">
<XCopy
:text="route.params.mesh"
>
<RouteTitle
:title="t('meshes.routes.item.title', { name: route.params.mesh })"
/>
</TextWithCopyButton>
</XCopy>
</h1>
</template>

Expand Down Expand Up @@ -53,5 +55,4 @@
<script lang="ts" setup>
import type { MeshSource } from '../sources'
import TextWithCopyButton from '@/app/common/TextWithCopyButton.vue'
</script>
9 changes: 9 additions & 0 deletions src/app/x/components/x-copy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# x-copy

## x-copy-debug

Wraps XCopy in a decorator to also `console.log` the text of any
copy (but not `console.error` the text if the copy fails).

The component should always be injected as a replacement for XCopy
in debug/dev modes only and never used directly.
73 changes: 73 additions & 0 deletions src/app/x/components/x-copy/XCopy.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<template>
<div
class="x-copy"
>
<span
v-if="props.format === 'custom' && props.text"
class="text"
>
<slot
name="default"
>
{{ props.text }}
</slot>
</span>
<KCopy
v-bind="attrs"
ref="component"
:class="{
'hidden': props.text.length === 0,
}"
:aria-hidden="props.text.length === 0"
:format="props.format === 'custom' ? 'hidden' : props.format"
:text="props.text.length === 0 ? _text : props.text"
/>
<slot
v-if="copy && props.text.length === 0"
name="default"
:copy="copy"
/>
</div>
</template>
<script lang="ts" setup>
import { KCopy } from '@kong/kongponents'
import { ref, nextTick, useAttrs } from 'vue'
type KCopyT = InstanceType<typeof KCopy>
type Format = KCopyT['$props']['format']
const attrs = useAttrs()
const component = ref<KCopyT | null>(null)
const _text = ref()
const copy = async (text: string) => {
_text.value = text
await nextTick()
component.value!.copy()
}
const props = withDefaults(defineProps<{
text?: string
format?: Format | 'custom'
}>(), {
text: '',
format: 'custom',
})
</script>
<style lang="scss" scoped>
.hidden {
display: none;
}
.x-copy {
display: inline-flex;
align-items: center;
gap: $kui-space-40;
}
.text {
min-width: 0;
word-wrap: break-word;
}
</style>
44 changes: 44 additions & 0 deletions src/app/x/components/x-copy/XCopyDebug.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<CopyButton
:text="props.text"
@mouseup="(e) => {
createCopy(() => {})(props.text)
}"
>
<template
#default="{ copy }"
>
<slot
name="default"
:copy="createCopy(copy)"
/>
</template>
</CopyButton>
</template>
<script lang="ts" setup>
import CopyButton from '@/app/x/components/x-copy/XCopy.vue'
const props = withDefaults(defineProps<{
text?: ''
}>(), {
text: '',
})
const createCopy = (copy: (text: string) => void) => (text: string) => {
console.info(
'%cdebug-k-clipboard-provider: The following was copied to the clipboard:', 'color: blue',
`
${text}`,
)
try {
// KCopy gives us no way to detect an error
// so this should never throw unless there is an error with the code
copy(text)
} catch (e) {
console.error(
'debug-k-clipboard-provider: The following wasn\'t copied to the clipboard:',
`
${text}`,
)
console.error(e)
}
}
</script>
2 changes: 2 additions & 0 deletions src/app/x/components/x-icon/XIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
PortalIcon,
MeshIcon,
BookIcon,
CopyIcon,
} from '@kong/icons'
import { KTooltip, PopPlacements } from '@kong/kongponents'
import { useSlots, useAttrs } from 'vue'
Expand All @@ -53,6 +54,7 @@ const icons = {
warning: WarningIcon,
mesh: MeshIcon,
docs: BookIcon,
copy: CopyIcon,
} as const
const id = uniqueId('-x-icon-tooltip')
const slots = useSlots()
Expand Down
3 changes: 3 additions & 0 deletions src/app/x/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import XAction from './components/x-action/XAction.vue'
import XBreadcrumbs from './components/x-breadcrumbs/XBreadcrumbs.vue'
import XCopy from './components/x-copy/XCopy.vue'
import XDisclosure from './components/x-disclosure/XDisclosure.vue'
import XIcon from './components/x-icon/XIcon.vue'
import XTabs from './components/x-tabs/XTabs.vue'
Expand All @@ -14,6 +15,7 @@ declare module '@vue/runtime-core' {
export interface GlobalComponents {
XIcon: typeof XIcon
XAction: typeof XAction
XCopy: typeof XCopy
XBreadcrumbs: typeof XBreadcrumbs
XTabs: typeof XTabs
XTeleportTemplate: typeof XTeleportTemplate
Expand All @@ -30,6 +32,7 @@ export const services = (app: Record<string, Token>): ServiceDefinition[] => {
return [
['XAction', XAction],
['XBreadcrumbs', XBreadcrumbs],
['XCopy', XCopy],
['XIcon', XIcon],
['XTabs', XTabs],
['XTeleportTemplate', XTeleportTemplate],
Expand Down
4 changes: 2 additions & 2 deletions src/services/development.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { setupWorker } from 'msw/browser'

import DebugKClipboardProvider from '@/app/application/components/debug-k-clipboard-provider/DebugKClipboardProvider.vue'
import debugI18n from '@/app/application/services/i18n/DebugI18n'
import { TOKENS as CONTROL_PLANES } from '@/app/control-planes'
import XCopyDebug from '@/app/x/components/x-copy/XCopyDebug.vue'
import cookied from '@/services/env/CookiedEnv'
import type Env from '@/services/env/Env'
import type { ServiceConfigurator, Token, TokenType } from '@/services/utils'
Expand Down Expand Up @@ -70,7 +70,7 @@ export const services: ServiceConfigurator<SupportedTokens> = (app) => [
[token('development.components'), {
service: () => {
return [
['KClipboardProvider', DebugKClipboardProvider],
['XCopy', XCopyDebug],
]
},
labels: [
Expand Down

0 comments on commit c1b1a8e

Please sign in to comment.