Skip to content
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
438 changes: 274 additions & 164 deletions src/Frontend/package-lock.json

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions src/Frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,26 @@
"test:application": "npm run test:application:vitest"
},
"dependencies": {
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/legacy-modes": "^6.5.0",
"@tinyhttp/content-disposition": "^2.2.2",
"@vue-flow/core": "^1.41.5",
"@wdns/vue-code-block": "^2.3.3",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"highlight.js": "^11.10.0",
"codemirror": "^6.0.1",
"lossless-json": "^4.0.2",
"memoize-one": "^6.0.0",
"moment": "^2.30.1",
"pinia": "^2.2.8",
"vue": "^3.5.13",
"vue-codemirror6": "^1.3.12",
"vue-router": "^4.4.5",
"vue-tippy": "^6.5.0",
"vue-toastification": "^2.0.0-rc.5",
"vue3-cookies": "^1.0.6",
"vue3-simple-typeahead": "^1.0.11"
"vue3-simple-typeahead": "^1.0.11",
"xml-formatter": "^3.6.4"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
Expand Down
76 changes: 76 additions & 0 deletions src/Frontend/src/components/CodeEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script setup lang="ts">
import CodeMirror from "vue-codemirror6";
import { json } from "@codemirror/lang-json";
import { xml } from "@codemirror/lang-xml";
import { StreamLanguage } from "@codemirror/language";
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { csharp } from "@codemirror/legacy-modes/mode/clike";
import { Extension } from "@codemirror/state";
import { CodeLanguage } from "@/components/codeEditorTypes";
import CopyToClipboard from "@/components/CopyToClipboard.vue";
import { computed } from "vue";
const code = defineModel<string>({ required: true });
const props = withDefaults(
defineProps<{
language?: CodeLanguage;
readOnly?: boolean;
showGutter?: boolean;
showCopyToClipboard?: boolean;
ariaLabel?: string;
}>(),
{ readOnly: true, showGutter: true, showCopyToClipboard: true }
);
const extensions = computed(() => {
const extensions: Extension[] = [];
switch (props.language) {
case "json":
extensions.push(json());
break;
case "xml":
extensions.push(xml());
break;
case "shell":
extensions.push(StreamLanguage.define(shell));
break;
case "powershell":
extensions.push(StreamLanguage.define(powerShell));
break;
case "csharp":
extensions.push(StreamLanguage.define(csharp));
break;
}
return extensions;
});
</script>

<template>
<div class="wrapper" :aria-label="ariaLabel">
<div v-if="props.showCopyToClipboard" class="toolbar">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment here to explain that the copy on CodeMirror doesn't seem to work so we rolled our own

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, out of the box, there is no copy button.
The last time I looked at a plugin option, I can't remember which one.
So, we don't need a comment here to spell that out.

<CopyToClipboard :value="code" />
</div>
<CodeMirror v-model="code" :extensions="extensions" :basic="props.showGutter" :minimal="!props.showGutter" :readonly="props.readOnly" :gutter="!props.readOnly"></CodeMirror>
</div>
</template>

<style scoped>
.wrapper {
border-radius: 0.5rem;
padding: 0.5rem;
border: 1px solid #ccc;
display: flex;
flex-direction: column;
}
.toolbar {
border-bottom: 1px solid #ccc;
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
display: flex;
flex-direction: row;
justify-content: end;
}
</style>
24 changes: 24 additions & 0 deletions src/Frontend/src/components/CopyToClipboard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { Tippy, TippyComponent } from "vue-tippy";
import { useTemplateRef } from "vue";

const props = defineProps<{
value: string;
}>();

const tippyRef = useTemplateRef<TippyComponent | null>("tippyRef");
let timeoutId: number;

async function copyToClipboard() {
await navigator.clipboard.writeText(props.value);
window.clearTimeout(timeoutId);
tippyRef.value?.show();
timeoutId = window.setTimeout(() => tippyRef.value?.hide(), 3000);
}
</script>

<template>
<Tippy content="Copied" ref="tippyRef" trigger="manual">
<button type="button" class="btn btn-secondary btn-sm" @click="copyToClipboard"><i class="fa fa-copy"></i> Copy to clipboard</button>
</Tippy>
</template>
1 change: 1 addition & 0 deletions src/Frontend/src/components/codeEditorTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type CodeLanguage = "json" | "xml" | "shell" | "powershell" | "csharp";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment these are the only languages supported, we can add more later if needed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enum?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

works the same

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ServiceControlNotAvailable from "../ServiceControlNotAvailable.vue";
import { licenseStatus } from "@/composables/serviceLicense";
import { connectionState, useServiceControlConnections } from "@/composables/serviceServiceControl";
import BusyIndicator from "../BusyIndicator.vue";
import VCodeBlock from "@wdns/vue-code-block";
import CodeEditor from "@/components/CodeEditor.vue";

const isExpired = licenseStatus.isExpired;

Expand Down Expand Up @@ -109,7 +109,7 @@ function switchJsonTab() {
<section v-if="showCodeOnlyTab && !loading">
<div class="row">
<div class="col-12 h-100">
<VCodeBlock :code="inlineSnippet" lang="csharp"></VCodeBlock>
<CodeEditor :model-value="inlineSnippet" language="csharp" :show-gutter="false"></CodeEditor>
</div>
</div>
</section>
Expand All @@ -119,11 +119,11 @@ function switchJsonTab() {
<div class="col-12 h-100">
<p>Note that when using JSON for configuration, you also need to change the endpoint configuration as shown below.</p>
<p><strong>Endpoint configuration:</strong></p>
<VCodeBlock :code="jsonSnippet" lang="csharp"></VCodeBlock>
<CodeEditor :model-value="jsonSnippet" language="csharp" :show-gutter="false"></CodeEditor>
<p style="margin-top: 15px">
<strong>JSON configuration file:</strong>
</p>
<VCodeBlock :code="jsonConfig" lang="json"></VCodeBlock>
<CodeEditor :model-value="jsonConfig" language="json" :show-gutter="false"></CodeEditor>
</div>
</div>
</section>
Expand Down
41 changes: 10 additions & 31 deletions src/Frontend/src/components/failedmessages/EditRetryDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import MessageHeader from "./EditMessageHeader.vue";
import { EditAndRetryConfig } from "@/resources/Configuration";
import type Header from "@/resources/Header";
import { ExtendedFailedMessage } from "@/resources/FailedMessage";
import parseContentType from "@/composables/contentTypeParser";
import { CodeLanguage } from "@/components/codeEditorTypes";
import CodeEditor from "@/components/CodeEditor.vue";

interface HeaderWithEditing extends Header {
isLocked: boolean;
Expand All @@ -28,6 +31,7 @@ interface LocalMessageState {
isBodyChanged: boolean;
isBodyEmpty: boolean;
isContentTypeSupported: boolean;
language?: CodeLanguage;
bodyContentType: string | undefined;
bodyUnavailable: boolean;
isEvent: boolean;
Expand Down Expand Up @@ -104,30 +108,6 @@ function getContentType() {
return header?.value;
}

function isContentTypeSupported(contentType: string | undefined) {
if (contentType === undefined) return false;

if (contentType.startsWith("text/")) return true;

const charsetUtf8 = "; charset=utf-8";

if (contentType.endsWith(charsetUtf8)) {
contentType = contentType.substring(0, contentType.length - charsetUtf8.length);
}

if (contentType === "application/json") return true;

if (contentType.startsWith("application/")) {
// Some examples:
// application/atom+xml
// application/ld+json
// application/vnd.masstransit+json
if (contentType.endsWith("+json") || contentType.endsWith("+xml")) return true;
}

return false;
}

Comment on lines -107 to -130
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor so it can be reused

function getMessageIntent() {
const intent = findHeadersByKey("NServiceBus.MessageIntent");
return intent?.value;
Expand Down Expand Up @@ -167,7 +147,9 @@ function initializeMessageBodyAndHeaders() {

const contentType = getContentType();
localMessage.value.bodyContentType = contentType;
localMessage.value.isContentTypeSupported = isContentTypeSupported(contentType);
const parsedContentType = parseContentType(contentType);
localMessage.value.isContentTypeSupported = parsedContentType.isSupported;
localMessage.value.language = parsedContentType.language;

const messageIntent = getMessageIntent();
localMessage.value.isEvent = messageIntent === "Publish";
Expand Down Expand Up @@ -248,7 +230,9 @@ onMounted(() => {
</tbody>
</table>
<div role="tabpanel" v-if="panel === 2 && !localMessage.bodyUnavailable" style="height: calc(100% - 260px)">
<textarea aria-label="message body" class="form-control" :disabled="!localMessage.isContentTypeSupported" v-model="localMessage.messageBody"></textarea>
<div style="margin-top: 1.25rem">
<CodeEditor aria-label="message body" :read-only="!localMessage.isContentTypeSupported" v-model="localMessage.messageBody" :language="localMessage.language" :show-gutter="true"></CodeEditor>
</div>
<span class="empty-error" v-if="localMessage.isBodyEmpty"><i class="fa fa-exclamation-triangle"></i> Message body cannot be empty</span>
<span class="reset-body" v-if="localMessage.isBodyChanged"><i class="fa fa-undo" v-tippy="`Reset changes`"></i> <a @click="resetBodyChanges()" href="javascript:void(0)">Reset changes</a></span>
<div class="alert alert-info" v-if="panel === 2 && localMessage.bodyUnavailable">{{ localMessage.bodyUnavailable }}</div>
Expand Down Expand Up @@ -369,9 +353,4 @@ onMounted(() => {
overflow-y: auto;
padding-right: 15px;
}

.modal-msg-editor :deep(textarea) {
height: 100%;
margin-top: 20px;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ onMounted(async () => {
await router.push({ path: routeLinks.messages.message.link(id.toString()), query: { back: routeLinks.failedMessage.failedMessages.link } });
});
</script>
<template>
<template></template>
</template>
9 changes: 7 additions & 2 deletions src/Frontend/src/components/messages/BodyView.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
<script setup lang="ts">
import { ExtendedFailedMessage } from "@/resources/FailedMessage";

import { computed } from "vue";
import CodeEditor from "@/components/CodeEditor.vue";
import parseContentType from "@/composables/contentTypeParser";
const props = defineProps<{
message: ExtendedFailedMessage;
}>();

const contentType = computed(() => parseContentType(props.message.contentType));
</script>

<template>
<div v-if="props.message.messageBodyNotFound" class="alert alert-info">Could not find message body. This could be because the message URL is invalid or the corresponding message was processed and is no longer tracked by ServiceControl.</div>
<div v-else-if="props.message.bodyUnavailable" class="alert alert-info">Message body unavailable.</div>
<pre v-else>{{ props.message.messageBody }}</pre>
<CodeEditor v-else-if="contentType.isSupported" :model-value="props.message.messageBody" :language="contentType.language" :read-only="true" :show-gutter="true"></CodeEditor>
<div v-else class="alert alert-warning">Message body cannot be displayed because content type "{{ props.message.contentType }}" is not supported.</div>
</template>

<style scoped></style>
Loading