Adding TenantPropertyService screen

This commit is contained in:
Peter Morton 2023-11-06 23:44:46 -06:00
parent 8c7658e090
commit aa738e1e66
16 changed files with 1343 additions and 152 deletions

View File

@ -1,2 +1,2 @@
VITE_API_BASE_URL=http://localhost:3000
VITE_API_BASE_URL=http://localhost:3000/api
VITE_ROUTER_BASE=/

901
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,11 +27,14 @@
"devDependencies": {
"@types/d3-sankey": "^0.12.1",
"@vitejs/plugin-vue": "^3.1.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.51.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-vue": "^9.15.1",
"postcss": "^8.4.31",
"prettier": "^2.8.8",
"sass": "^1.63.6",
"tailwindcss": "^3.3.5",
"vite": "^3.1.0"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1,68 @@
<template>
<Transition>
<div
v-if="modalActive"
id="createProductModal"
tabindex="-1"
aria-hidden="true"
class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-fulloverflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full flex"
>
<div class="relative p-4 w-full max-w-2xl max-h-full">
<!-- Modal content -->
<div
class="relative p-4 bg-white rounded-lg shadow dark:bg-gray-800 sm:p-5"
>
<!-- Modal header -->
<div
class="flex justify-between items-center pb-4 mb-4 rounded-t border-b sm:mb-5 dark:border-gray-600"
>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
Add Property
</h3>
<button
type="button"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
data-modal-target="createProductModal"
data-modal-toggle="createProductModal"
@click="$emit('close-modal')"
>
<svg
aria-hidden="true"
class="w-5 h-5"
fill="currentColor"
viewbox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<slot />
</div>
</div>
</div>
</Transition>
<div
v-if="modalActive"
modal-backdrop=""
class="bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40"
></div>
</template>
<script setup>
defineEmits(["close-modal"]);
defineProps({
modalActive: {
type: Boolean,
default: false,
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,62 @@
<template>
<table class="w-full text-left text-sm text-gray-500 dark:text-gray-400">
<thead
class="bg-gray-50 text-xs uppercase text-gray-700 dark:bg-gray-700 dark:text-gray-400"
>
<tr>
<th v-for="column in columns" :key="column.key" class="px-4 py-4">
{{ column.label }}
</th>
<th class="px-4 py-4">action</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in rows"
:key="row.name"
class="border-b dark:border-gray-700"
>
<td v-for="column in columns" :key="column.key" class="px-4 py-3">
{{ row[column.key] }}
</td>
<td class="flex items-center px-4 py-3">
<button
class="flex items-center justify-center rounded-lg bg-blue-700 px-4 py-2 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800 mr-2"
@click="editProperty(row.name)"
>
Edit
</button>
<button
class="flex items-center justify-center rounded-lg bg-red-700 px-4 py-2 text-sm font-medium text-white hover:bg-red-800 focus:outline-none focus:ring-4 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800"
@click="deleteProperty(row.name)"
>
Delete
</button>
</td>
</tr>
</tbody>
</table>
</template>
<script setup>
const emit = defineEmits(["edit", "delete"]);
defineProps({
rows: {
type: Array,
default: null,
},
columns: {
type: Array,
default: null,
},
});
const editProperty = (name) => {
emit("edit", name);
};
const deleteProperty = (name) => {
emit("delete", name);
};
</script>

View File

@ -0,0 +1,73 @@
<script setup>
import { ref, onMounted } from "vue";
import useTenantPropertiesStore from "@/stores/tenantPropertiesStore";
const emit = defineEmits(["close-modal"]);
const props = defineProps({
username: { type: String, default: "" },
sessionIdentifier: { type: String, default: "" },
name: {
type: String,
default: null,
},
});
const propertiesStore = useTenantPropertiesStore(props);
const property = ref({
name: "",
value: "",
lastEditedBy: "",
lastEditedDate: "",
});
const performAction = () => {
updateProperty();
};
const updateProperty = () => {
propertiesStore.updateProperty(property.value);
resetProperty();
emit("close-modal");
};
const resetProperty = () => {
property.value = {};
};
onMounted(() => {
if (props.name) {
propertiesStore.getPropertyByName(props.name).then((fetchedProperty) => {
property.value = fetchedProperty;
});
}
});
</script>
<template>
<form @submit.prevent="performAction">
<div class="grid gap-4 mb-4 sm:grid-cols-2">
<div>
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
<input v-model="property.name" type="text"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Type property name" required="" />
</div>
<div class="sm:col-span-2">
<label for="value" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Value</label>
<textarea id="value" v-model="property.value" rows="4"
class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="Enter value here"></textarea>
</div>
</div>
<button type="submit"
class="text-white inline-flex items-center bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800">
{{ props.name ? "Update" : "Add" }} Property
</button>
</form>
</template>
<style lang="scss" scoped></style>

View File

@ -28,7 +28,7 @@ export function getTenantProperty(key, props) {
var authKey = getAuthKeyFromProperties(props);
console.log(`Fetching TPS Property [${key}]`);
fetch(`${apiBaseUrl}/tps?propertyName=${key}&authKey=${authKey}`, {
fetch(`${apiBaseUrl}/tps/${key}?authKey=${authKey}`, {
credentials: "include", // fetch won't send cookies unless you set credentials
})
.then((response) => {

View File

@ -3,28 +3,28 @@ import { routerBase } from "../app.config.js";
import HomeView from "../views/HomeView.vue";
import AboutView from "../views/AboutView.vue";
import DebugFrameView from "../views/DebugFrameView.vue";
import SideBarView from "../views/SideBarView.vue";
import SearchByReferenceView from "../views/SearchByReferenceView.vue";
import InteractionsFlowView from "../views/InteractionsFlowView.vue";
import CustomerAccountView from "../views/TelephonyContextView.vue";
import TelephonyContextView from "../views/TelephonyContextView.vue";
import TenantPropertiesView from "../views/TenantPropertiesView.vue";
const routes = [
{
path: "/",
name: "home",
components: { default: HomeView, SideBarView: SideBarView },
props: { default: false, SideBarView: true },
components: { default: HomeView },
props: { default: false },
},
{
path: "/about",
name: "about",
components: { default: AboutView, SideBarView: SideBarView },
components: { default: AboutView },
props: { default: false, SideBarView: true },
},
{
path: "/referenceId",
name: "referenceId",
components: { default: SearchByReferenceView, SideBarView: SideBarView },
components: { default: SearchByReferenceView },
props: {
default: (route) => ({
sessionIdentifier: route.query._sessionIdentifier,
@ -34,7 +34,7 @@ const routes = [
{
path: "/interactionsFlow",
name: "interactionsFlow",
components: { default: InteractionsFlowView, SideBarView: SideBarView },
components: { default: InteractionsFlowView },
props: {
default: (route) => ({
sessionIdentifier: route.query._sessionIdentifier,
@ -45,7 +45,7 @@ const routes = [
{
path: "/telephonyContext",
name: "telephonyContext",
components: { default: CustomerAccountView },
components: { default: TelephonyContextView },
props: {
default: (route) => ({
...route.params,
@ -61,6 +61,18 @@ const routes = [
components: { default: DebugFrameView },
props: { default: true },
},
{
path: "/tenantProperties",
name: "tenantProperties",
components: { default: TenantPropertiesView },
props: {
default: (route) => ({
...route.params,
...route.query,
sessionIdentifier: route.query._sessionIdentifier,
}),
},
},
];
console.log(`mounting router on ${routerBase}`);

View File

@ -0,0 +1,120 @@
import { getAuthKeyFromProperties } from "../helpers/index.js"
import { apiBaseUrl } from "../app.config.js";
export default function useTenantPropertiesStore(props) {
const authKey = getAuthKeyFromProperties(props);
const store = {
authKey: authKey,
getColumns: function () {
return [
{
key: 'name',
label: 'Name'
},
{
key: 'value',
label: 'Value'
},
{
key: 'lastModifiedBy',
label: 'Last Modified By'
},
{
key: 'lastModifiedDate',
label: 'Last Modified Date'
}
]
},
getProperties: function () {
return new Promise((resolve, reject) => {
fetch(`${apiBaseUrl}/tps?authKey=${authKey}`, {
credentials: "include", // fetch won't send cookies unless you set credentials
}).then((response) => {
// check for error response
if (!response.ok) {
reject(response.data || response.statusText);
}
response.json().then((data) => {
resolve(data.data);
});
})
.catch((error) => {
console.error(error);
reject(error);
});
})
},
getPropertyByName: function (name) {
return new Promise((resolve, reject) => {
fetch(`${apiBaseUrl}/tps/${name}?authKey=${authKey}`, {
credentials: "include", // fetch won't send cookies unless you set credentials
}).then((response) => {
// check for error response
if (!response.ok) {
reject(response.data || response.statusText);
}
response.json().then((data) => {
console.log("Found Property:" + JSON.stringify(data));
resolve(data.data);
});
})
.catch((error) => {
console.error(error);
reject(error);
});
})
},
updateProperty: (property) => {
return new Promise((resolve, reject) => {
const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: property }),
};
fetch(`${apiBaseUrl}/tps?authKey=${authKey}`, requestOptions)
.then((response) => {
// check for error response
if (!response.ok) {
reject(response.data || response.statusText);
}
response.json().then((data) => {
console.log("Found Property:" + JSON.stringify(data));
resolve(data.data);
});
})
.catch((error) => {
console.error(error);
reject(error);
});
})
},
deleteProperty: (name) => {
return new Promise((resolve, reject) => {
const requestOptions = {
method: "DELETE"
};
fetch(`${apiBaseUrl}/tps/${name}?authKey=${authKey}`, requestOptions)
.then((response) => {
// check for error response
if (!response.ok) {
reject(response.data || response.statusText);
}
response.json().then((data) => {
console.log("Found Property:" + JSON.stringify(data));
resolve(data.data);
});
})
.catch((error) => {
console.error(error);
reject(error);
});
})
}
}
return store;
}

View File

@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
h1 {
font-size: 18px;
line-height: 19px;

View File

@ -1,6 +1,5 @@
<script setup>
const transferSummaryHTML = `Janice Jones from Acme.com authenticated using onetime passcode authentication. They successfully changed the quantity on latest order from 10 to 12. She wants to dicuss with someone to alter payment arrangements payment due date. Transferring to queue.`;
const scriptElementName = "script";
const integrationCardDoc = `<html>
@ -83,7 +82,9 @@ const integrationCardDoc = `<html>
</script>
<template>
<div class="home">
<h1>Engagement Orchestration - Microservice Examples</h1>
<h1 class="text-3xl font-bold underline">
Engagement Orchestration - Microservice Examples
</h1>
<h2>Introduction</h2>
<p>
This site provides a set of microservices to augment Engagement
@ -111,11 +112,15 @@ const integrationCardDoc = `<html>
transferSummary: transferSummaryHTML,
integrationCardDoc: integrationCardDoc,
integrationCardTitle: 'Integration Card',
crsScore: 72,
},
}"
>Telephony Context</router-link
>
</li>
<li>
<router-link to="/tenantProperties">Tenant Properties</router-link>
</li>
</ul>
</div>
</template>

View File

@ -1,99 +0,0 @@
<script>
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { SidebarMenu } from "vue-sidebar-menu";
import "vue-sidebar-menu/dist/vue-sidebar-menu.css";
import { h, markRaw } from "vue";
const separator = {
template: '<hr style="border-color: rgba(0, 0, 0, 0.1); margin: 20px;">',
};
const faIcon = (props) => {
return {
element: markRaw({
render: () => h("div", [h(FontAwesomeIcon, { size: "lg", ...props })]),
}),
};
};
export default {
name: "SideBarView",
components: {
SidebarMenu,
},
// props: {
// sidebar: {
// type: Boolean,
// required: false
// }
// },
data() {
return {
menu: [
{
header: "Services",
hiddenOnCollapse: true,
},
{
href: "/",
title: "Home",
icon: faIcon({ icon: "fa-solid fa-house" }),
},
{
href: "/about",
title: "About",
icon: faIcon({ icon: "fa-solid fa-question" }),
},
{
component: markRaw(separator),
},
{
href: "/referenceId",
title: "Contacts Lookup",
icon: faIcon({ icon: "fa-solid fa-rectangle-list" }),
},
{
href: "/interactionsFlow",
title: "Interactions Flow",
icon: faIcon({ icon: "fa-solid fa-route" }),
},
{
href: "/customerAccount/12345",
title: "Customer Account Example",
icon: faIcon({ icon: "fa-solid fa-id-card" }),
},
],
collapsed: false,
};
},
mounted() {
this.onResize();
window.addEventListener("resize", this.onResize);
console.log();
},
methods: {
onResize() {
if (window.innerWidth <= 767) {
this.isOnMobile = true;
this.collapsed = true;
} else {
this.isOnMobile = false;
this.collapsed = false;
}
},
},
};
</script>
<template>
<div v-if="$route.query.sidebar" id="sidebar">
<sidebar-menu
v-model:collapsed="collapsed"
:menu="menu"
:show-one-child="true"
:relative="true"
/>
</div>
</template>
<style></style>

View File

@ -0,0 +1,107 @@
<script setup>
import { ref, computed, onMounted, onUpdated } from "vue";
import DataTable from "@/components/DataTable.vue";
import BaseModel from "@/components/BaseModal.vue";
import TenantPropertyForm from "@/components/TenantPropertyForm.vue";
import useTenantPropertiesStore from "@/stores/tenantPropertiesStore";
const props = defineProps({
username: { type: String, default: "" },
sessionIdentifier: { type: String, default: "" }
});
const modalActive = ref(null);
const name = ref(null);
const tenantPropertiesStore = useTenantPropertiesStore(props);
const columns = computed(() => tenantPropertiesStore.getColumns());
const rows = ref(null)
onMounted(() => {
tenantPropertiesStore.getProperties().then((properties) => { rows.value = properties });
})
const toggleModal = () => {
modalActive.value = !modalActive.value;
if (!modalActive.value) {
name.value = null;
}
tenantPropertiesStore.getProperties().then((properties) => { rows.value = properties });
};
const openEditModal = (id) => {
name.value = id;
toggleModal();
};
const deleteProperty = (name) => {
tenantPropertiesStore.deleteProperty(name);
};
const refreshData = () => {
tenantPropertiesStore.getProperties().then((properties) => { rows.value = properties });
}
</script>
<template>
<!-- Start block -->
<section class="bg-gray-50 p-3 antialiased dark:bg-gray-900 sm:p-5">
<div class="mx-auto max-w-screen-xl px-4 lg:px-12">
<!-- Start coding here -->
<div
class="relative overflow-hidden bg-white shadow-md dark:bg-gray-800 sm:rounded-lg"
>
<nav
class="flex flex-col items-start justify-between space-y-3 p-4 md:flex-row md:items-center md:space-y-0"
aria-label="Table navigation"
>
<h2
class="mb-4 text-3xl font-extrabold leading-none tracking-tight text-gray-900 dark:text-white md:text-4xl"
>
Tenant Properties
</h2>
</nav>
<div
class="flex flex-col items-center justify-between space-y-3 p-4 md:flex-row md:space-x-4 md:space-y-0"
>
<div
class="flex w-full flex-shrink-0 flex-col items-stretch justify-end space-y-2 md:w-auto md:flex-row md:items-center md:space-x-3 md:space-y-0"
>
<button
id="createProductModalButton"
type="button"
data-modal-target="createProductModal"
data-modal-toggle="createProductModal"
class="flex items-center justify-center rounded-lg bg-green-700 px-4 py-2 text-sm font-medium text-white hover:bg-green-800 focus:outline-none focus:ring-4 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800"
@click="toggleModal"
>
Add Property
</button>
<button
id="refreshButton"
type="button"
class="flex items-center justify-center rounded-lg bg-green-700 px-4 py-2 text-sm font-medium text-white hover:bg-green-800 focus:outline-none focus:ring-4 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800"
@click="refreshData"
>
Refresh Data
</button>
</div>
</div>
<div class="overflow-x-auto">
<DataTable
:rows="rows"
:columns="columns"
@edit="openEditModal"
@delete="deleteProperty"
/>
</div>
</div>
</div>
<BaseModel :modal-active="modalActive" @close-modal="toggleModal">
<TenantPropertyForm v-bind="props" v-model:name="name" @close-modal="toggleModal" />
</BaseModel>
</section>
</template>

8
tailwind.config.js Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["index.html", "src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

View File

@ -1,4 +1,5 @@
import { defineConfig } from "vite";
import { fileURLToPath, URL } from "node:url";
import vue from "@vitejs/plugin-vue";
import { createHtmlPlugin } from "vite-plugin-html";
@ -36,6 +37,7 @@ export default defineConfig(({ command }) => {
resolve: {
alias: {
vue: "vue/dist/vue.esm-bundler",
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
};
@ -72,6 +74,7 @@ export default defineConfig(({ command }) => {
resolve: {
alias: {
vue: "vue/dist/vue.esm-bundler",
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
};