mirror of
https://github.com/beak-insights/felicity-lims.git
synced 2025-02-22 16:03:00 +08:00
added websocket update repost generation complete
This commit is contained in:
parent
d3c3abbfae
commit
2cd8ab17b3
18 changed files with 155 additions and 64 deletions
|
@ -1,7 +1,10 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from datetime import datetime
|
||||
import strawberry # noqa
|
||||
|
||||
from felicity.api.gql.analysis.types.analysis import AnalysisType
|
||||
from felicity.api.gql.user.types import UserType
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class Nothing:
|
||||
|
@ -73,3 +76,23 @@ class LaggardData:
|
|||
@strawberry.type
|
||||
class LaggardStatistics:
|
||||
data: List[LaggardData]
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ReportMetaType:
|
||||
uid: int
|
||||
period_start: datetime
|
||||
period_end: datetime
|
||||
date_column: str
|
||||
location: Optional[str]
|
||||
sample_states: Optional[str]
|
||||
report_type: str
|
||||
status: Optional[str]
|
||||
temp: Optional[str]
|
||||
analyses: Optional[List[AnalysisType]]
|
||||
created_at: Optional[datetime]
|
||||
created_by_uid: Optional[int]
|
||||
created_by: Optional[UserType]
|
||||
updated_at: Optional[datetime]
|
||||
updated_by_uid: Optional[int]
|
||||
updated_by: Optional[UserType]
|
|
@ -4,11 +4,13 @@ from typing import List, Optional, Union
|
|||
import strawberry # noqa
|
||||
from felicity.api.gql.analysis.types.analysis import SampleType
|
||||
from felicity.api.gql.analysis.types.results import AnalysisResultType
|
||||
from felicity.api.gql.analytics.types import ReportMetaType
|
||||
from felicity.api.gql.setup.types import DepartmentType
|
||||
from felicity.api.gql.user.types import GroupType, UserType
|
||||
from felicity.api.gql.worksheet.types import WorkSheetType
|
||||
from felicity.apps.analysis.models.analysis import Sample
|
||||
from felicity.apps.analysis.models.results import AnalysisResult
|
||||
from felicity.apps.analytics.models import ReportMeta
|
||||
from felicity.apps.worksheet.models import WorkSheet
|
||||
|
||||
|
||||
|
@ -19,7 +21,7 @@ class UnknownObjectType:
|
|||
|
||||
actionObject = strawberry.union(
|
||||
"actionObject",
|
||||
[WorkSheetType, SampleType, AnalysisResultType],
|
||||
[WorkSheetType, SampleType, AnalysisResultType, ReportMetaType],
|
||||
description="Union of possible object types for streams",
|
||||
)
|
||||
|
||||
|
@ -50,7 +52,7 @@ class ActivityStreamType:
|
|||
@strawberry.field
|
||||
async def action_object(
|
||||
self, info
|
||||
) -> Union[WorkSheetType, SampleType, AnalysisResultType, UnknownObjectType]:
|
||||
) -> Union[WorkSheetType, SampleType, AnalysisResultType, ReportMetaType, UnknownObjectType]:
|
||||
if self.action_object_type == "sample":
|
||||
sample = await Sample.get(uid=self.action_object_uid)
|
||||
return SampleType(
|
||||
|
@ -70,6 +72,12 @@ class ActivityStreamType:
|
|||
exclude=["right", "left", "tree_id", "level", "worksheet"]
|
||||
), parent=None)
|
||||
|
||||
if self.action_object_type == "report":
|
||||
report = await ReportMeta.get(uid=self.action_object_uid)
|
||||
return ReportMetaType(**report.marshal_simple(
|
||||
exclude=[]
|
||||
))
|
||||
|
||||
return UnknownObjectType(
|
||||
message=f"Please provide a resolver for object of type {self.action_object_type}"
|
||||
)
|
||||
|
|
|
@ -6,12 +6,13 @@ from felicity.apps.analysis.models.analysis import Sample
|
|||
from felicity.apps.analytics import SampleAnalyticsInit, conf, models
|
||||
from felicity.apps.job import conf as job_conf
|
||||
from felicity.apps.job import models as job_models
|
||||
from felicity.apps.notification.utils import ReportNotifier
|
||||
from felicity.apps.notification.utils import ReportNotifier, FelicityStreamer
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
report_notifier = ReportNotifier()
|
||||
streamer = FelicityStreamer()
|
||||
|
||||
|
||||
async def generate_report(job_uid: str):
|
||||
|
@ -67,4 +68,5 @@ async def generate_report(job_uid: str):
|
|||
f"Your {report.report_type} report was successfully generated",
|
||||
report.created_by,
|
||||
)
|
||||
await streamer.stream(report, report.created_by, "generated", "report")
|
||||
return True
|
||||
|
|
|
@ -37,8 +37,18 @@ const authFromStorage2 = (): {
|
|||
}
|
||||
|
||||
const authLogout = () => {
|
||||
|
||||
localStorage.removeItem(STORAGE_AUTH_KEY);
|
||||
}
|
||||
|
||||
export { authToStorage, authFromStorage,authFromStorage2, authLogout }
|
||||
|
||||
const getAuthData = () => {
|
||||
let data: any = {};
|
||||
if(localStorage.getItem(STORAGE_AUTH_KEY)){
|
||||
const auth = JSON.parse(localStorage.getItem(STORAGE_AUTH_KEY)!)
|
||||
data = { auth }
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
export { authToStorage, authFromStorage,authFromStorage2, authLogout, getAuthData }
|
|
@ -1,26 +1,25 @@
|
|||
import axios from 'axios';
|
||||
import { useAuthStore } from "../stores"
|
||||
import { getAuthData, authLogout } from "../auth"
|
||||
|
||||
import { REST_BASE_URL } from '../conf'
|
||||
|
||||
const getAuthHeaders = async () => {
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const authData = getAuthData();
|
||||
|
||||
if (authStore?.auth?.token) {
|
||||
if (authData?.auth?.token) {
|
||||
return {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PATCH, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Origin, Content-Type, X-Auth-Token',
|
||||
...(authStore?.auth?.token && {
|
||||
...(authData?.auth?.token && {
|
||||
'x-felicity-user-id': "felicity-user",
|
||||
'x-felicity-role': "felicity-administrator",
|
||||
'Authorization': `Bearer ${authStore?.auth?.token}`
|
||||
'Authorization': `Bearer ${authData?.auth?.token}`
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
authStore.logout();
|
||||
authLogout();
|
||||
};
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
|
|
|
@ -25,6 +25,5 @@
|
|||
import { ref } from 'vue';
|
||||
|
||||
let show = ref(false);
|
||||
|
||||
</script>
|
||||
|
||||
|
|
56
frontend/vite/src/composables/analytics.ts
Normal file
56
frontend/vite/src/composables/analytics.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { toRefs, reactive } from 'vue'
|
||||
import axios from "../axios/with-auth";
|
||||
|
||||
import useNotifyToast from "./alert_toast";
|
||||
import { IReportListing } from '../models/reports';
|
||||
|
||||
const { toastSuccess, toastWarning } = useNotifyToast();
|
||||
|
||||
const state = reactive({
|
||||
reports: [],
|
||||
} as {
|
||||
reports: IReportListing[];
|
||||
});
|
||||
|
||||
export default function useAnalyticsComposable() {
|
||||
|
||||
const fetchReports = async () => {
|
||||
await axios.get("reports").then((resp) => {
|
||||
state.reports = resp.data;
|
||||
})
|
||||
}
|
||||
|
||||
const generateReport = async (payload) => {
|
||||
await axios.post("reports", payload).then((resp) => {
|
||||
state.reports.push(resp.data);
|
||||
})
|
||||
}
|
||||
|
||||
const deleteReport = async (report: IReportListing) => {
|
||||
await axios.delete("reports/" + report.uid).then((resp) => {
|
||||
const data = resp.data;
|
||||
const index = state.reports.findIndex((x) => x.uid === data.uid);
|
||||
if (index > -1) {
|
||||
state.reports.splice(index, 1);
|
||||
toastSuccess(data.message);
|
||||
} else {
|
||||
toastWarning("Failed to remove report: Please refresh your page");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateReport = (report: IReportListing) => {
|
||||
const index = state.reports.findIndex((x) => x.uid === report.uid);
|
||||
if (index > -1) {
|
||||
state.reports[index] = {...state.reports[index], ...report }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
fetchReports,
|
||||
generateReport,
|
||||
deleteReport,
|
||||
updateReport
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import userPreferenceComposable from "./preferences"
|
|||
import useReportComposable from "./reports"
|
||||
import useSampleComposable from "./samples"
|
||||
import useWorkSheetComposable from "./worksheet"
|
||||
// import useAnalyticsComposable from "./analytics"
|
||||
|
||||
export {
|
||||
useNotifyToast,
|
||||
|
@ -13,5 +14,6 @@ export {
|
|||
userPreferenceComposable,
|
||||
useReportComposable,
|
||||
useSampleComposable,
|
||||
useWorkSheetComposable
|
||||
useWorkSheetComposable,
|
||||
// useAnalyticsComposable
|
||||
}
|
|
@ -66,6 +66,11 @@ subscription getSystemActivity {
|
|||
result
|
||||
status
|
||||
}
|
||||
...on ReportMetaType {
|
||||
uid
|
||||
status
|
||||
location
|
||||
}
|
||||
}
|
||||
targetUid
|
||||
verb
|
||||
|
|
|
@ -28,6 +28,7 @@ pinia.use(({ store }) => {
|
|||
});
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(pinia)
|
||||
app.component('font-awesome-icon', FontAwesomeIcon)
|
||||
app.component('default-layout', LayoutDashboard)
|
||||
app.component('empty-layout', LayoutEmpty)
|
||||
|
@ -35,6 +36,5 @@ app.use(urql, urqlClient)
|
|||
app.use(VueSweetalert2)
|
||||
app.use(MotionPlugin)
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.mount('#app')
|
||||
|
||||
|
|
|
@ -14,6 +14,9 @@ export interface IUser {
|
|||
isSuperuser?: boolean;
|
||||
authUid?: number;
|
||||
auth?: IUserAuth;
|
||||
// for API axios
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
}
|
||||
|
||||
export interface IUserAuth {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
interface IPageInfo {
|
||||
export interface IPageInfo {
|
||||
endCursor?: string,
|
||||
hasNextPage?: boolean,
|
||||
hasPreviousPage?: boolean,
|
||||
|
|
|
@ -38,7 +38,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
|
||||
const logout = () => {
|
||||
toastInfo("Good bye " + auth.value.user?.firstName)
|
||||
localStorage.removeItem(STORAGE_AUTH_KEY)
|
||||
reset()
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { GET_ALL_CLIENTS,
|
|||
GET_CLIENT_BY_UID } from '../graphql/clients.queries';
|
||||
import { addListsUnique } from '../utils';
|
||||
import { IClient, IClientContact } from '../models/client'
|
||||
import { IPageInfo } from '../models/pagination'
|
||||
|
||||
import { useApiUtil } from '../composables'
|
||||
|
||||
|
@ -20,7 +21,7 @@ export const useClientStore = defineStore('client', {
|
|||
clientContacts: [],
|
||||
fetchingClientContacts: false,
|
||||
clientCount: 0,
|
||||
clientPageInfo: undefined,
|
||||
clientPageInfo: {},
|
||||
} as {
|
||||
clients: IClient[];
|
||||
fetchingClients: boolean;
|
||||
|
@ -29,7 +30,7 @@ export const useClientStore = defineStore('client', {
|
|||
clientContacts: IClientContact[];
|
||||
fetchingClientContacts: boolean;
|
||||
clientCount?: number;
|
||||
clientPageInfo?: any;
|
||||
clientPageInfo?: IPageInfo;
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
import { pipe, subscribe } from 'wonka';
|
||||
import { useWorksheetStore } from './worksheet'
|
||||
import { useSampleStore } from './sample'
|
||||
import useAnalyticsComposable from '../composables/analytics';
|
||||
|
||||
|
||||
export const useStreamStore = defineStore('stream', {
|
||||
|
@ -19,11 +20,12 @@ export const useStreamStore = defineStore('stream', {
|
|||
},
|
||||
actions: {
|
||||
addStream(payload){
|
||||
const { updateReport } = useAnalyticsComposable()
|
||||
const wsStore = useWorksheetStore()
|
||||
const sampleStore = useSampleStore()
|
||||
|
||||
this.streams?.unshift(payload);
|
||||
|
||||
|
||||
if(payload.actionObjectType === "sample"){
|
||||
sampleStore.updateSampleStatus(payload.actionObject)
|
||||
}
|
||||
|
@ -32,6 +34,10 @@ export const useStreamStore = defineStore('stream', {
|
|||
wsStore.updateWorksheetStatus(payload.actionObject)
|
||||
}
|
||||
|
||||
if(payload.actionObjectType === "report"){
|
||||
updateReport(payload.actionObject)
|
||||
}
|
||||
|
||||
if(payload.actionObjectType === "result"){
|
||||
sampleStore.updateAnalysesResultsStatus([payload.actionObject])
|
||||
wsStore.updateAnalysesResults([payload.actionObject])
|
||||
|
|
|
@ -16,7 +16,7 @@ import { authExchange } from '@urql/exchange-auth';
|
|||
import { SubscriptionClient } from 'subscriptions-transport-ws'
|
||||
import { pipe, tap } from 'wonka'
|
||||
|
||||
import { useAuthStore } from "./stores"
|
||||
import { getAuthData, authLogout } from "./auth"
|
||||
import { GQL_BASE_URL, WS_BASE_URL } from './conf'
|
||||
import { useNotifyToast } from './composables'
|
||||
|
||||
|
@ -27,13 +27,13 @@ const subscriptionClient = new SubscriptionClient( WS_BASE_URL, {
|
|||
reconnect: true,
|
||||
lazy: true,
|
||||
connectionParams: () => {
|
||||
const authStore = useAuthStore();
|
||||
const authData = getAuthData();
|
||||
return {
|
||||
headers: {
|
||||
...(authStore?.auth?.token && {
|
||||
...(authData?.auth?.token && {
|
||||
'x-felicity-user-id': "felicity-user-x",
|
||||
'x-felicity-role': "felicity-role-x",
|
||||
'Authorization': `Bearer ${authStore?.auth?.token}`
|
||||
'Authorization': `Bearer ${authData?.auth?.token}`
|
||||
})
|
||||
},
|
||||
}
|
||||
|
@ -41,11 +41,11 @@ const subscriptionClient = new SubscriptionClient( WS_BASE_URL, {
|
|||
});
|
||||
|
||||
const getAuth = async ({ authState }) => {
|
||||
const authStore = useAuthStore();
|
||||
const authData = getAuthData();
|
||||
|
||||
if (!authState) {
|
||||
if (authStore?.auth?.token) {
|
||||
return { token: authStore?.auth?.token };
|
||||
if (authData?.auth?.token) {
|
||||
return { token: authData?.auth?.token };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ const getAuth = async ({ authState }) => {
|
|||
|
||||
toastError("Faied to get Auth Data. Login");
|
||||
|
||||
authStore.logout();
|
||||
authLogout();
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -118,7 +118,7 @@ export const urqlClient = createClient({
|
|||
}
|
||||
if (isAuthError) {
|
||||
toastError("Unknown Network Error Encountered")
|
||||
useAuthStore().logout();
|
||||
authLogout();
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
@ -135,16 +135,16 @@ export const urqlClient = createClient({
|
|||
}),
|
||||
],
|
||||
fetchOptions: () => {
|
||||
const authStore = useAuthStore();
|
||||
const authData = getAuthData();
|
||||
return {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PATCH, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Origin, Content-Type, X-Auth-Token',
|
||||
...(authStore?.auth?.token && {
|
||||
...(authData?.auth?.token && {
|
||||
'x-felicity-user-id': "felicity-user-x",
|
||||
'x-felicity-role': "felicity-role-x",
|
||||
'Authorization': `Bearer ${authStore?.auth?.token}`
|
||||
'Authorization': `Bearer ${authData?.auth?.token}`
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
|
||||
function showMoreClients(): void {
|
||||
clientParams.first = +clientBatch.value;
|
||||
clientParams.after = clientPageInfo?.value?.endCursor;
|
||||
clientParams.after = clientPageInfo?.value?.endCursor!;
|
||||
clientParams.text = filterText.value;
|
||||
clientParams.filterAction = false;
|
||||
clientStore.fetchClients(clientParams);
|
||||
|
@ -186,11 +186,11 @@
|
|||
<div class="my-4 flex sm:flex-row flex-col">
|
||||
<button @click.prevent="showMoreClients()"
|
||||
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800rounded-smtransition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none"
|
||||
:disabled="!pageInfo?.hasNextPage">Show More</button>
|
||||
:disabled="!clientPageInfo?.hasNextPage">Show More</button>
|
||||
<div class="flex flex-row mb-1 sm:mb-0">
|
||||
<div class="relative">
|
||||
<select class="appearance-none h-full rounded-l-sm border block w-full bg-white border-gray-400 text-gray-700 py-2 px-4 pr-8 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||
v-model="clientBatch" :disabled="!pageInfo?.hasNextPage">
|
||||
v-model="clientBatch" :disabled="!clientPageInfo?.hasNextPage">
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import accordion from "../../components/Accordion.vue";
|
||||
import { reactive, onMounted } from "vue";
|
||||
import axios from "../../axios/with-auth";
|
||||
import { REST_BASE_URL } from "../../conf";
|
||||
import { useNotifyToast } from "../../composables";
|
||||
import { useAnalysisStore } from "../../stores";
|
||||
import { IReportListing } from "../../models/reports";
|
||||
import useAnalyticsComposable from "../../composables/analytics"
|
||||
|
||||
const { toastSuccess, toastWarning } = useNotifyToast();
|
||||
const analysisStore = useAnalysisStore();
|
||||
const { reports, fetchReports, generateReport, deleteReport } = useAnalyticsComposable()
|
||||
|
||||
const state = reactive({
|
||||
reports: [] as IReportListing[],
|
||||
listingForm: {
|
||||
report_type: "",
|
||||
analyses_uids: [],
|
||||
|
@ -29,17 +27,10 @@ onMounted(async () => {
|
|||
text: "",
|
||||
sortBy: ["name"],
|
||||
});
|
||||
await axios.get("reports").then((resp) => {
|
||||
state.reports = resp.data;
|
||||
});
|
||||
await fetchReports();
|
||||
});
|
||||
|
||||
const saveListingForm = () => {
|
||||
const request = { ...state.listingForm };
|
||||
axios.post("reports", request).then((resp) => {
|
||||
state.reports.push(resp.data);
|
||||
});
|
||||
};
|
||||
const saveListingForm = () => generateReport({ ...state.listingForm });
|
||||
|
||||
const downloadReport = (report: any) => {
|
||||
const link = document.createElement("a");
|
||||
|
@ -49,19 +40,6 @@ const downloadReport = (report: any) => {
|
|||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const deleteReport = (report: any) => {
|
||||
axios.delete("reports/" + report.uid).then((resp) => {
|
||||
const data = resp.data;
|
||||
const index = state.reports.findIndex((x) => x.uid === data.uid);
|
||||
if (index > -1) {
|
||||
state!.reports.splice(index, 1);
|
||||
toastSuccess(data.message);
|
||||
} else {
|
||||
toastWarning("Failed to remove report: Please refresh your page");
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -70,7 +48,7 @@ const deleteReport = (report: any) => {
|
|||
<template v-slot:body>
|
||||
<div class="overflow-x-auto mt-4">
|
||||
<div
|
||||
v-if="state.reports?.length > 0"
|
||||
v-if="reports?.length > 0"
|
||||
class="align-middle inline-block min-w-full shadow overflow-hidden bg-white shadow-dashboard px-2 pt-1 rounded-bl-sm rounded-br-sm"
|
||||
>
|
||||
<table class="min-w-full">
|
||||
|
@ -115,7 +93,7 @@ const deleteReport = (report: any) => {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white">
|
||||
<tr v-for="report in state.reports" :key="report.uid">
|
||||
<tr v-for="report in reports" :key="report.uid">
|
||||
<td>
|
||||
<input type="checkbox" />
|
||||
</td>
|
||||
|
|
Loading…
Reference in a new issue