Skip to content

Commit a9fe62a

Browse files
authored
Merge pull request #2659 from Particular/john/commonality
2 parents 73e78b3 + 50824df commit a9fe62a

31 files changed

+544
-195
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Set up Node.js
2828
uses: actions/[email protected]
2929
with:
30-
node-version: 22.19.x
30+
node-version: 22.x
3131
- name: Build Frontend
3232
run: .\build.ps1
3333
working-directory: src/ServicePulse.Host

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Set up Node.js
3434
uses: actions/[email protected]
3535
with:
36-
node-version: 22.19.x
36+
node-version: 22.x
3737
- name: Build Frontend
3838
run: .\build.ps1
3939
working-directory: src/ServicePulse.Host
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<script setup lang="ts">
2+
import FAIcon from "@/components/FAIcon.vue";
3+
import { IconDefinition, faRefresh } from "@fortawesome/free-solid-svg-icons";
4+
5+
export type ButtonVariant = "primary" | "secondary" | "danger" | "link" | "default";
6+
export type ButtonSize = "sm" | "lg" | "default";
7+
8+
interface Props {
9+
variant?: ButtonVariant;
10+
size?: ButtonSize;
11+
icon?: IconDefinition;
12+
iconPosition?: "left" | "right";
13+
disabled?: boolean;
14+
loading?: boolean;
15+
tooltip?: string;
16+
ariaLabel?: string;
17+
type?: "button" | "submit" | "reset";
18+
}
19+
20+
const props = withDefaults(defineProps<Props>(), {
21+
variant: "default",
22+
size: "default",
23+
iconPosition: "left",
24+
disabled: false,
25+
loading: false,
26+
type: "button",
27+
});
28+
29+
const variantClasses = {
30+
primary: "btn-primary",
31+
secondary: "btn-secondary",
32+
danger: "btn-danger",
33+
link: "btn-link",
34+
default: "btn-default",
35+
};
36+
37+
const sizeClasses = {
38+
sm: "btn-sm",
39+
lg: "btn-lg",
40+
default: "",
41+
};
42+
</script>
43+
44+
<template>
45+
<button
46+
class="btn"
47+
:class="[variantClasses[props.variant], sizeClasses[props.size], { disabled: props.disabled || props.loading }]"
48+
:disabled="props.disabled || props.loading"
49+
:type="props.type"
50+
:aria-label="props.ariaLabel"
51+
v-tippy="props.tooltip"
52+
>
53+
<FAIcon v-if="props.icon && props.iconPosition === 'left' && !props.loading" :icon="props.icon" class="icon-left" />
54+
<FAIcon v-if="props.loading" class="rotate" :icon="faRefresh" />
55+
<span v-if="$slots.default" class="button-text">
56+
<slot />
57+
</span>
58+
<FAIcon v-if="props.icon && props.iconPosition === 'right' && !props.loading" :icon="props.icon" class="icon-right" />
59+
</button>
60+
</template>
61+
62+
<style scoped>
63+
.btn {
64+
display: inline-flex;
65+
align-items: center;
66+
gap: 0.375rem;
67+
cursor: pointer;
68+
}
69+
70+
.btn.disabled {
71+
cursor: not-allowed;
72+
opacity: 0.65;
73+
}
74+
75+
.icon-left,
76+
.icon-right {
77+
color: var(--reduced-emphasis);
78+
}
79+
80+
.icon-left {
81+
margin-right: 0.25rem;
82+
}
83+
84+
.icon-right {
85+
margin-left: 0.25rem;
86+
}
87+
88+
.rotate {
89+
animation: spin 1s linear infinite;
90+
}
91+
92+
@keyframes spin {
93+
from {
94+
transform: rotate(0deg);
95+
}
96+
to {
97+
transform: rotate(360deg);
98+
}
99+
}
100+
101+
.button-text {
102+
flex: 1;
103+
}
104+
</style>

src/Frontend/src/components/ConfirmDialog.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import ActionButton from "@/components/ActionButton.vue";
23
import { onMounted, onUnmounted } from "vue";
34
45
const emit = defineEmits<{ confirm: []; cancel: [] }>();
@@ -46,8 +47,8 @@ onMounted(() => {
4647
<p v-if="secondParagraph && secondParagraph.length">{{ secondParagraph }}</p>
4748
</div>
4849
<div class="modal-footer">
49-
<button class="btn btn-primary" :aria-label="hideCancel ? 'Ok' : 'Yes'" @click="confirm">{{ hideCancel ? "Ok" : "Yes" }}</button>
50-
<button v-if="!hideCancel" aria-label="No" class="btn btn-default" @click="close">No</button>
50+
<ActionButton variant="primary" :aria-label="hideCancel ? 'Ok' : 'Yes'" @click="confirm">{{ hideCancel ? "Ok" : "Yes" }}</ActionButton>
51+
<ActionButton v-if="!hideCancel" aria-label="No" @click="close">No</ActionButton>
5152
</div>
5253
</div>
5354
</div>

src/Frontend/src/components/CopyToClipboard.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import { Tippy, TippyComponent } from "vue-tippy";
33
import { ref, useTemplateRef, watch } from "vue";
4-
import FAIcon from "@/components/FAIcon.vue";
4+
import ActionButton from "@/components/ActionButton.vue";
55
import { faCopy } from "@fortawesome/free-regular-svg-icons";
66
77
const props = withDefaults(
@@ -27,7 +27,7 @@ watch(timeoutId, (_, previousTimeoutId) => clearTimeout(previousTimeoutId));
2727

2828
<template>
2929
<Tippy content="Copied" ref="tippyRef" trigger="manual">
30-
<button v-if="!props.isIconOnly" type="button" class="btn btn-secondary btn-sm" @click="copyToClipboard"><FAIcon :icon="faCopy" /> Copy to clipboard</button>
31-
<button v-else type="button" class="btn btn-secondary btn-sm" @click="copyToClipboard" v-tippy="'Copy to clipboard'"><FAIcon :icon="faCopy" /></button>
30+
<ActionButton v-if="!props.isIconOnly" variant="secondary" size="sm" :icon="faCopy" @click="copyToClipboard">Copy to clipboard</ActionButton>
31+
<ActionButton v-else variant="secondary" size="sm" :icon="faCopy" tooltip="Copy to clipboard" @click="copyToClipboard" />
3232
</Tippy>
3333
</template>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script setup lang="ts">
2+
import FAIcon from "@/components/FAIcon.vue";
3+
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
4+
5+
interface Props {
6+
icon?: IconDefinition;
7+
}
8+
9+
defineProps<Props>();
10+
</script>
11+
12+
<template>
13+
<span class="metadata">
14+
<FAIcon v-if="icon" :icon="icon" class="icon" />
15+
<slot />
16+
</span>
17+
</template>
18+
19+
<style scoped>
20+
.metadata {
21+
display: inline-flex;
22+
align-items: center;
23+
gap: 0.25rem;
24+
}
25+
26+
.icon {
27+
margin-right: 0.25rem;
28+
}
29+
</style>

src/Frontend/src/components/RefreshConfig.vue

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import { ref, watch } from "vue";
33
import ListFilterSelector from "@/components/audit/ListFilterSelector.vue";
4-
import FAIcon from "@/components/FAIcon.vue";
4+
import ActionButton from "@/components/ActionButton.vue";
55
import { faRefresh } from "@fortawesome/free-solid-svg-icons";
66
77
const props = defineProps<{ isLoading: boolean }>();
@@ -62,7 +62,7 @@ async function refresh() {
6262

6363
<template>
6464
<div class="refresh-config">
65-
<button class="btn btn-sm" title="refresh" @click="refresh"><FAIcon class="refresh-icon" :class="{ spinning: showSpinning }" :icon="faRefresh" /> Refresh List</button>
65+
<ActionButton size="sm" :icon="faRefresh" :loading="showSpinning" @click="refresh">Refresh List</ActionButton>
6666
<div class="filter">
6767
<div class="filter-label">Auto-Refresh:</div>
6868
<div class="filter-component">
@@ -88,23 +88,4 @@ async function refresh() {
8888
.filter-label {
8989
font-weight: bold;
9090
}
91-
92-
@keyframes spin {
93-
from {
94-
transform: rotate(0deg);
95-
}
96-
to {
97-
transform: rotate(360deg);
98-
}
99-
}
100-
101-
.refresh-icon {
102-
display: inline-block;
103-
color: var(--reduced-emphasis);
104-
}
105-
106-
/* You can add this class dynamically when needed */
107-
.refresh-icon.spinning {
108-
animation: spin 1s linear infinite;
109-
}
11091
</style>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script setup lang="ts">
2+
import TypeNameDisplay from "@/components/TypeNameDisplay.vue";
3+
4+
interface Props {
5+
sagaType: string;
6+
maxWidth?: string;
7+
ellipsesStyle?: "RightSide" | "LeftSide";
8+
showTitle?: boolean;
9+
titleLevel?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div";
10+
cssClass?: string;
11+
}
12+
13+
withDefaults(defineProps<Props>(), {
14+
maxWidth: "182px",
15+
ellipsesStyle: "LeftSide",
16+
showTitle: false,
17+
titleLevel: "div",
18+
cssClass: "sagaName",
19+
});
20+
</script>
21+
22+
<template>
23+
<component :is="showTitle ? titleLevel : 'div'" :class="cssClass" :aria-label="showTitle ? 'saga name' : undefined">
24+
<TypeNameDisplay :type-name="sagaType" :max-width="maxWidth" :ellipses-style="ellipsesStyle" />
25+
</component>
26+
</template>
27+
28+
<style scoped>
29+
.sagaName {
30+
color: #e6e6e6;
31+
}
32+
</style>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<script setup lang="ts">
2+
import FAIcon from "@/components/FAIcon.vue";
3+
import { IconDefinition, faCheck, faTimes, faExclamationTriangle, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
4+
5+
export type StatusType = "success" | "error" | "warning" | "info";
6+
7+
interface Props {
8+
status: StatusType;
9+
message?: string;
10+
icon?: IconDefinition;
11+
size?: "2xs" | "xs" | "sm" | "lg" | "xl" | "2xl" | "1x" | "2x" | "3x" | "4x" | "5x" | "6x" | "7x" | "8x" | "9x" | "10x";
12+
showMessage?: boolean;
13+
customClass?: string;
14+
}
15+
16+
const props = withDefaults(defineProps<Props>(), {
17+
message: "",
18+
size: "1x",
19+
showMessage: true,
20+
customClass: "",
21+
});
22+
23+
const statusConfig = {
24+
success: {
25+
icon: faCheck,
26+
class: "text-success",
27+
defaultMessage: "Success",
28+
},
29+
error: {
30+
icon: faTimes,
31+
class: "text-danger",
32+
defaultMessage: "Error",
33+
},
34+
warning: {
35+
icon: faExclamationTriangle,
36+
class: "text-warning",
37+
defaultMessage: "Warning",
38+
},
39+
info: {
40+
icon: faInfoCircle,
41+
class: "text-info",
42+
defaultMessage: "Info",
43+
},
44+
};
45+
46+
const currentConfig = statusConfig[props.status];
47+
const displayIcon = props.icon || currentConfig.icon;
48+
const displayMessage = props.message || currentConfig.defaultMessage;
49+
const cssClass = props.customClass || currentConfig.class;
50+
</script>
51+
52+
<template>
53+
<span :class="['status-icon', cssClass]">
54+
<FAIcon :icon="displayIcon" :size="size" :title="showMessage ? displayMessage : undefined" />
55+
<span v-if="showMessage && message" class="status-message">{{ displayMessage }}</span>
56+
</span>
57+
</template>
58+
59+
<style scoped>
60+
.status-icon {
61+
display: inline-flex;
62+
align-items: center;
63+
gap: 0.25rem;
64+
}
65+
66+
.text-success {
67+
color: #28a745;
68+
}
69+
70+
.text-danger {
71+
color: #dc3545;
72+
}
73+
74+
.text-warning {
75+
color: #ffc107;
76+
}
77+
78+
.text-info {
79+
color: #17a2b8;
80+
}
81+
82+
.info-color {
83+
color: #17a2b8;
84+
}
85+
86+
.error-color {
87+
color: #dc3545;
88+
}
89+
90+
.status-message {
91+
margin-left: 0.25rem;
92+
}
93+
</style>

0 commit comments

Comments
 (0)