Skip to content

Commit 73bbeef

Browse files
author
sadnub
committed
frontend branding init
1 parent f55a0ff commit 73bbeef

6 files changed

Lines changed: 363 additions & 5 deletions

File tree

src/components/modals/coresettings/EditCoreSettings.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<q-tab name="retention" label="Retention" />
1515
<q-tab name="apikeys" label="API Keys" />
1616
<q-tab name="sso" label="Single Sign-On (SSO)" />
17+
<q-tab name="branding" label="Branding" />
1718
<!-- <q-tab name="openai" label="Open AI" /> -->
1819
</q-tabs>
1920
</template>
@@ -643,6 +644,11 @@
643644
<SSOProvidersTable />
644645
</q-tab-panel>
645646

647+
<!-- branding -->
648+
<q-tab-panel name="branding">
649+
<BrandSettings />
650+
</q-tab-panel>
651+
646652
<!-- Open AI -->
647653
<!-- <q-tab-panel name="openai">
648654
<div class="text-subtitle2">Open AI</div>
@@ -690,10 +696,13 @@
690696
<q-card-section class="row items-center">
691697
<q-btn
692698
v-show="
699+
tab !== 'apikeys' &&
700+
tab !== 'webhooks' &&
693701
tab !== 'customfields' &&
694702
tab !== 'keystore' &&
695703
tab !== 'urlactions' &&
696-
tab !== 'sso'
704+
tab !== 'sso' &&
705+
tab !== 'branding'
697706
"
698707
label="Save"
699708
color="primary"
@@ -732,6 +741,7 @@ import URLActionsTable from "@/components/modals/coresettings/URLActionsTable.vu
732741
import APIKeysTable from "@/components/core/APIKeysTable.vue";
733742
import SSOProvidersTable from "@/ee/sso/components/SSOProvidersTable.vue";
734743
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
744+
import BrandSettings from "@/ee/whitelabel/components/BrandSettings.vue";
735745
736746
export default {
737747
name: "EditCoreSettings",
@@ -743,6 +753,7 @@ export default {
743753
APIKeysTable,
744754
SSOProvidersTable,
745755
TacticalDropdown,
756+
BrandSettings,
746757
},
747758
mixins: [mixins],
748759
data() {

src/ee/whitelabel/api.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
Copyright (c) 2023-present Amidaware Inc.
3+
This file is subject to the EE License Agreement.
4+
For details, see: https://license.tacticalrmm.com/ee
5+
*/
6+
7+
import { ref } from "vue";
8+
import axios from "axios";
9+
import type { Branding } from "./types";
10+
import { notifySuccess } from "@/utils/notify";
11+
12+
export function useBrandingStore() {
13+
const branding = ref<Branding>({
14+
company_name: "",
15+
primary_color: "",
16+
secondary_color: "",
17+
accent_color: "",
18+
dark_color: "",
19+
dark_page_color: "",
20+
positive_color: "",
21+
negative_color: "",
22+
info_color: "",
23+
warning_color: "",
24+
favicon: "",
25+
});
26+
27+
const isLoading = ref(false);
28+
29+
function getBranding() {
30+
isLoading.value = true;
31+
32+
axios
33+
.get<Branding>("/core/branding/")
34+
.then((response) => {
35+
branding.value = response.data;
36+
})
37+
.catch(() => {
38+
//
39+
})
40+
.finally(() => {
41+
isLoading.value = false;
42+
});
43+
}
44+
45+
async function saveBranding(data: Branding) {
46+
isLoading.value = true;
47+
try {
48+
await axios.post("/core/branding/", data);
49+
branding.value = data;
50+
notifySuccess("Branding saved successfully");
51+
} catch (error) {
52+
//
53+
} finally {
54+
isLoading.value = false;
55+
}
56+
}
57+
58+
return {
59+
branding,
60+
isLoading,
61+
getBranding,
62+
saveBranding,
63+
};
64+
}
65+
66+
export const brandingStore = useBrandingStore();
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<!--
2+
Copyright (c) 2023-present Amidaware Inc.
3+
This file is subject to the EE License Agreement.
4+
For details, see: https://license.tacticalrmm.com/ee
5+
-->
6+
7+
<template>
8+
<template v-if="!$branding">
9+
Custom branding feature requires a Tier 2 or higher sponsorship. Please
10+
check the docs for more info.
11+
</template>
12+
<template v-else>
13+
<q-card flat>
14+
<div style="max-height: 50vh" class="scroll">
15+
<q-card-section class="row">
16+
<div class="col-4">Company Name</div>
17+
<div class="col-2"></div>
18+
<q-input filled dense v-model="branding.company_name" class="col-6" />
19+
</q-card-section>
20+
<q-card-section class="row">
21+
<div class="col-4">Primary Color:</div>
22+
<div class="col-2"></div>
23+
<q-input filled dense v-model="branding.primary_color" class="col-6">
24+
<template v-slot:append>
25+
<q-icon name="colorize" class="cursor-pointer">
26+
<q-popup-proxy
27+
cover
28+
transition-show="scale"
29+
transition-hide="scale"
30+
>
31+
<q-color v-model="branding.primary_color" />
32+
</q-popup-proxy>
33+
</q-icon>
34+
</template>
35+
</q-input>
36+
</q-card-section>
37+
<q-card-section class="row">
38+
<div class="col-4">Secondary Color:</div>
39+
<div class="col-2"></div>
40+
<q-input
41+
filled
42+
dense
43+
v-model="branding.secondary_color"
44+
class="col-6"
45+
>
46+
<template v-slot:append>
47+
<q-icon name="colorize" class="cursor-pointer">
48+
<q-popup-proxy
49+
cover
50+
transition-show="scale"
51+
transition-hide="scale"
52+
>
53+
<q-color v-model="branding.secondary_color" />
54+
</q-popup-proxy>
55+
</q-icon>
56+
</template>
57+
</q-input>
58+
</q-card-section>
59+
<q-card-section class="row">
60+
<div class="col-4">Accent Color:</div>
61+
<div class="col-2"></div>
62+
<q-input filled dense v-model="branding.accent_color" class="col-6">
63+
<template v-slot:append>
64+
<q-icon name="colorize" class="cursor-pointer">
65+
<q-popup-proxy
66+
cover
67+
transition-show="scale"
68+
transition-hide="scale"
69+
>
70+
<q-color v-model="branding.accent_color" />
71+
</q-popup-proxy>
72+
</q-icon>
73+
</template>
74+
</q-input>
75+
</q-card-section>
76+
<q-card-section class="row">
77+
<div class="col-4">Dark Color:</div>
78+
<div class="col-2"></div>
79+
<q-input filled dense v-model="branding.dark_color" class="col-6">
80+
<template v-slot:append>
81+
<q-icon name="colorize" class="cursor-pointer">
82+
<q-popup-proxy
83+
cover
84+
transition-show="scale"
85+
transition-hide="scale"
86+
>
87+
<q-color v-model="branding.dark_color" />
88+
</q-popup-proxy>
89+
</q-icon>
90+
</template>
91+
</q-input>
92+
</q-card-section>
93+
<q-card-section class="row">
94+
<div class="col-4">Dark Page Color:</div>
95+
<div class="col-2"></div>
96+
<q-input
97+
filled
98+
dense
99+
v-model="branding.dark_page_color"
100+
class="col-6"
101+
>
102+
<template v-slot:append>
103+
<q-icon name="colorize" class="cursor-pointer">
104+
<q-popup-proxy
105+
cover
106+
transition-show="scale"
107+
transition-hide="scale"
108+
>
109+
<q-color v-model="branding.dark_color" />
110+
</q-popup-proxy>
111+
</q-icon>
112+
</template>
113+
</q-input>
114+
</q-card-section>
115+
<q-card-section class="row">
116+
<div class="col-4">Positive Color:</div>
117+
<div class="col-2"></div>
118+
<q-input filled dense v-model="branding.positive_color" class="col-6">
119+
<template v-slot:append>
120+
<q-icon name="colorize" class="cursor-pointer">
121+
<q-popup-proxy
122+
cover
123+
transition-show="scale"
124+
transition-hide="scale"
125+
>
126+
<q-color v-model="branding.positive_color" />
127+
</q-popup-proxy>
128+
</q-icon>
129+
</template>
130+
</q-input>
131+
</q-card-section>
132+
<q-card-section class="row">
133+
<div class="col-4">Negative Color:</div>
134+
<div class="col-2"></div>
135+
<q-input filled dense v-model="branding.negative_color" class="col-6">
136+
<template v-slot:append>
137+
<q-icon name="colorize" class="cursor-pointer">
138+
<q-popup-proxy
139+
cover
140+
transition-show="scale"
141+
transition-hide="scale"
142+
>
143+
<q-color v-model="branding.negative_color" />
144+
</q-popup-proxy>
145+
</q-icon>
146+
</template>
147+
</q-input>
148+
</q-card-section>
149+
<q-card-section class="row">
150+
<div class="col-4">Info Color:</div>
151+
<div class="col-2"></div>
152+
<q-input filled dense v-model="branding.info_color" class="col-6">
153+
<template v-slot:append>
154+
<q-icon name="colorize" class="cursor-pointer">
155+
<q-popup-proxy
156+
cover
157+
transition-show="scale"
158+
transition-hide="scale"
159+
>
160+
<q-color v-model="branding.info_color" />
161+
</q-popup-proxy>
162+
</q-icon>
163+
</template>
164+
</q-input>
165+
</q-card-section>
166+
<q-card-section class="row">
167+
<div class="col-4">Warning Color:</div>
168+
<div class="col-2"></div>
169+
<q-input filled dense v-model="branding.warning_color" class="col-6">
170+
<template v-slot:append>
171+
<q-icon name="colorize" class="cursor-pointer">
172+
<q-popup-proxy
173+
cover
174+
transition-show="scale"
175+
transition-hide="scale"
176+
>
177+
<q-color v-model="branding.warning_color" />
178+
</q-popup-proxy>
179+
</q-icon>
180+
</template>
181+
</q-input>
182+
</q-card-section>
183+
<q-card-section class="row">
184+
<div class="col-4">Favicon:</div>
185+
<div class="col-2"></div>
186+
<q-file
187+
class="col-6"
188+
v-model="localFavicon"
189+
filled
190+
label="Favicon (png, ico, svg)"
191+
accept=".svg, .png, .ico"
192+
@update:model-value="fileChanged"
193+
clearable
194+
dense
195+
>
196+
<template v-slot:after>
197+
<q-img
198+
v-if="branding.favicon !== null"
199+
:src="branding.favicon"
200+
height="32px"
201+
width="32px"
202+
/>
203+
</template>
204+
</q-file>
205+
</q-card-section>
206+
</div>
207+
<q-card-actions align="right">
208+
<q-btn label="Save" color="primary" @click="submit" />
209+
</q-card-actions>
210+
<q-inner-loading :showing="isLoading">
211+
<q-spinner color="primary" size="3em" />
212+
</q-inner-loading>
213+
</q-card>
214+
</template>
215+
</template>
216+
217+
<script lang="ts" setup>
218+
import { ref, onMounted } from "vue";
219+
import { brandingStore } from "../api";
220+
221+
const localFavicon = ref<File | null>(null);
222+
223+
const { branding, isLoading } = brandingStore;
224+
225+
// setup stores
226+
async function submit() {
227+
try {
228+
await brandingStore.saveBranding(branding.value);
229+
} catch {
230+
// do nothing
231+
}
232+
}
233+
234+
async function fileChanged(file: File | null) {
235+
if (!file) {
236+
branding.value.favicon = "";
237+
return;
238+
}
239+
branding.value.favicon = await fileToDataUrl(file);
240+
}
241+
242+
function fileToDataUrl(file: File): Promise<string> {
243+
return new Promise((resolve, reject) => {
244+
const reader = new FileReader();
245+
reader.onerror = () => reject(reader.error);
246+
reader.onload = () => resolve(String(reader.result));
247+
reader.readAsDataURL(file);
248+
});
249+
}
250+
251+
onMounted(() => {
252+
brandingStore.getBranding();
253+
});
254+
</script>

src/ee/whitelabel/types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
Copyright (c) 2023-present Amidaware Inc.
3+
This file is subject to the EE License Agreement.
4+
For details, see: https://license.tacticalrmm.com/ee
5+
*/
6+
7+
export interface Branding {
8+
company_name: string;
9+
primary_color: string;
10+
secondary_color: string;
11+
accent_color: string;
12+
dark_color: string;
13+
dark_page_color: string;
14+
positive_color: string;
15+
negative_color: string;
16+
info_color: string;
17+
warning_color: string;
18+
favicon: string;
19+
}

0 commit comments

Comments
 (0)