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

chore: add XCopyButton component #2531

Merged
merged 1 commit into from
May 21, 2024
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

This file was deleted.

This file was deleted.

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

<KCopy
format="hidden"
:text="props.text"
/>
</div>
<XCopyButton
format="custom"
:text="props.text"
>
<template
v-if="$slots.default"
#default
>
<slot name="default" />
</template>
</XCopyButton>
</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>
<XCopyButton
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"
/>
</XCopyButton>
</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">
<XCopyButton
:text="route.params.mesh"
>
<RouteTitle
:title="t('meshes.routes.item.title', { name: route.params.mesh })"
/>
</TextWithCopyButton>
</XCopyButton>
</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-button/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# x-copy-button

## x-copy-button-debug

Wraps XCopyButton 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 XCopyButton in
debug/dev modes only and never used directly.
73 changes: 73 additions & 0 deletions src/app/x/components/x-copy-button/XCopyButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<template>
<div
class="x-copy-button"
>
<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-button {
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-button/XCopyButtonDebug.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-button/XCopyButton.vue'
const props = withDefaults(defineProps<{
text?: ''
}>(), {
text: '',
})
const createCopy = (copy: (text: string) => void) => (text: string) => {
console.info(
'%cx-copy-button-debug: 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(
'x-copy-button-debug: 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 @@ -35,6 +35,7 @@ import {
MeshIcon,
BookIcon,
FilterIcon,
CopyIcon,
} from '@kong/icons'
import { KTooltip, PopPlacements } from '@kong/kongponents'
import { useSlots, useAttrs } from 'vue'
Expand All @@ -55,6 +56,7 @@ const icons = {
mesh: MeshIcon,
docs: BookIcon,
search: FilterIcon,
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 XCopyButton from './components/x-copy-button/XCopyButton.vue'
import XDisclosure from './components/x-disclosure/XDisclosure.vue'
import XIcon from './components/x-icon/XIcon.vue'
import XInput from './components/x-input/XInput.vue'
Expand All @@ -16,6 +17,7 @@ declare module '@vue/runtime-core' {
XIcon: typeof XIcon
XInput: typeof XInput
XAction: typeof XAction
XCopyButton: typeof XCopyButton
XBreadcrumbs: typeof XBreadcrumbs
XTabs: typeof XTabs
XTeleportTemplate: typeof XTeleportTemplate
Expand All @@ -32,6 +34,7 @@ export const services = (app: Record<string, Token>): ServiceDefinition[] => {
return [
['XAction', XAction],
['XBreadcrumbs', XBreadcrumbs],
['XCopyButton', XCopyButton],
['XIcon', XIcon],
['XInput', XInput],
['XTabs', XTabs],
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 XCopyButtonDebug from '@/app/x/components/x-copy-button/XCopyButtonDebug.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],
['XCopyButton', XCopyButtonDebug],
]
},
labels: [
Expand Down