Radu Liviu Carjan
3 years ago
32 changed files with 27613 additions and 7011 deletions
-
113app/Http/Controllers/FileController.php
-
21app/Http/Controllers/FilterController.php
-
10app/Http/Controllers/HomeController.php
-
24app/Http/Controllers/PagesController.php
-
183package-lock.json
-
2package.json
-
BINpublic/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-500.woff
-
BINpublic/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-500.woff2
-
BINpublic/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-700.woff
-
BINpublic/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-700.woff2
-
BINpublic/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-regular.woff
-
BINpublic/fonts/vendor/primevue/resources/themes/mdc-dark-indigo/roboto-v20-latin-ext_latin-regular.woff2
-
32878public/js/app.js
-
33resources/js/app.ts
-
100resources/js/components/Home.vue
-
42resources/js/components/Home/Home.ts
-
35resources/js/components/Home/Home.vue
-
84resources/js/components/ProcessFile.vue
-
11resources/js/components/ProcessFile/ProcessFile.scss
-
121resources/js/components/ProcessFile/ProcessFile.ts
-
111resources/js/components/ProcessFile/ProcessFile.vue
-
69resources/js/components/helpers/Filter.vue
-
7resources/js/interfaces/FilterInterface.ts
-
4resources/js/interfaces/FilterOptions.ts
-
18resources/js/plugins/ApiPlugin.ts
-
7resources/js/plugins/plugins.d.ts
-
60resources/js/services/ApiService.ts
-
1resources/sass/app.scss
-
2resources/views/pages/home.blade.php
-
2routes/api.php
-
5routes/web.php
-
7webpack.mix.js
@ -1,24 +0,0 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers; |
|||
|
|||
use Illuminate\View\View; |
|||
use Illuminate\Contracts\View\Factory; |
|||
use Illuminate\Contracts\Container\BindingResolutionException; |
|||
|
|||
class PagesController extends Controller |
|||
{ |
|||
|
|||
/** |
|||
* Return the home page view |
|||
* |
|||
* @return View|Factory |
|||
* |
|||
* @throws BindingResolutionException |
|||
*/ |
|||
public function home(): View |
|||
{ |
|||
$filters = FilterController::$filters; |
|||
return view('pages.home')->with('filters', $filters); |
|||
} |
|||
} |
32878
public/js/app.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,100 +0,0 @@ |
|||
<template> |
|||
<div class="wrap" v-if="!fileUploaded && !uploading"> |
|||
<Toast position="top-right" /> |
|||
|
|||
<Panel header="Please upload a file"> |
|||
<FileUpload |
|||
name="demo[]" |
|||
:customUpload="true" |
|||
:auto="true" |
|||
@uploader="uploadFile" |
|||
> |
|||
<template #empty> |
|||
<p>Drag and drop files to here to upload.</p> |
|||
</template> |
|||
</FileUpload> |
|||
</Panel> |
|||
|
|||
<BlockUI :blocked="uiBlocked" :fullScreen="true"></BlockUI> |
|||
</div> |
|||
<div class="wrap" v-else-if="!fileUploaded && uploading"> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
</div> |
|||
<div class="wrap" v-else> |
|||
<process-file :file="uploadResult" :filters="filters"></process-file> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import axios from 'axios'; |
|||
// import Vue from 'vue'; |
|||
// import Component from 'vue-class-component'; |
|||
import { Vue, Component, Prop } from 'vue-property-decorator'; |
|||
|
|||
@Component |
|||
export default class Home extends Vue { |
|||
|
|||
@Prop({ default: [] }) |
|||
public readonly filters!: Array<object> |
|||
|
|||
public uiBlocked = false; |
|||
public uploading = false; |
|||
public fileUploaded: boolean = false; |
|||
public uploadResult = null; |
|||
|
|||
/** |
|||
* |
|||
*/ |
|||
public created() |
|||
{ |
|||
console.log(this.filters); |
|||
} |
|||
|
|||
/** |
|||
* A method which uploads the files to the server for processing |
|||
* |
|||
* @param event The event containing the uploaded files information |
|||
*/ |
|||
public uploadFile(event: any): void { |
|||
this.uploading = true; |
|||
this.fileUploaded = false; |
|||
|
|||
|
|||
this['$toast'].add({severity:'success', summary: 'Success Message', detail:'Order submitted', life: 3000}); |
|||
let file = event.files[0]; |
|||
|
|||
let formData = new FormData(); |
|||
formData.append('file', file); |
|||
|
|||
setTimeout( |
|||
() => { |
|||
axios.post( |
|||
'http://core.sandd/api/file', |
|||
formData, |
|||
{ |
|||
headers: { |
|||
'Content-Type': 'multipart/form-data' |
|||
} |
|||
} |
|||
).then( |
|||
(response) => { |
|||
this.fileUploaded = true; |
|||
this.uploadResult = response.data; |
|||
// console.log('Success: ', response); |
|||
} |
|||
).catch( |
|||
(err) => { |
|||
console.log('Error: ', err); |
|||
} |
|||
); |
|||
}, 500 |
|||
) |
|||
} |
|||
} |
|||
</script> |
@ -0,0 +1,42 @@ |
|||
import axios from 'axios'; |
|||
import { Vue, Component, Prop } from 'vue-property-decorator'; |
|||
|
|||
@Component |
|||
export default class Home extends Vue { |
|||
|
|||
@Prop({ default: [] }) |
|||
public readonly searchers!: { [key: string]: string; } |
|||
|
|||
public uiBlocked = false; |
|||
public uploading = false; |
|||
public fileUploaded: boolean = false; |
|||
public uploadResult = null; |
|||
|
|||
/** |
|||
* |
|||
*/ |
|||
public created() |
|||
{} |
|||
|
|||
/** |
|||
* A method which uploads the files to the server for processing |
|||
* |
|||
* @param event The event containing the uploaded files information |
|||
*/ |
|||
public uploadFile(event: any): void { |
|||
this.uploading = true; |
|||
this.fileUploaded = false; |
|||
|
|||
|
|||
this.$toast.add({severity:'success', summary: 'Success Message', detail:'Order submitted', life: 3000}); |
|||
let file = event.files[0]; |
|||
|
|||
setTimeout( |
|||
async () => { |
|||
let response = await this.$api.uploadFile(file); |
|||
this.fileUploaded = true; |
|||
this.uploadResult = response; |
|||
}, 500 |
|||
) |
|||
} |
|||
} |
@ -0,0 +1,35 @@ |
|||
<template> |
|||
<div class="wrap" v-if="!fileUploaded && !uploading"> |
|||
<Toast position="top-right" /> |
|||
|
|||
<Panel header="Please upload a file"> |
|||
<FileUpload |
|||
name="demo[]" |
|||
:customUpload="true" |
|||
:auto="true" |
|||
@uploader="uploadFile" |
|||
> |
|||
<template #empty> |
|||
<p>Drag and drop files to here to upload.</p> |
|||
</template> |
|||
</FileUpload> |
|||
</Panel> |
|||
|
|||
<BlockUI :blocked="uiBlocked" :fullScreen="true"></BlockUI> |
|||
</div> |
|||
<div class="wrap" v-else-if="!fileUploaded && uploading"> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
<Skeleton /> |
|||
</div> |
|||
<div class="wrap" v-else> |
|||
<process-file :file="uploadResult" :searchers="searchers"></process-file> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts" src="./Home.ts"> |
|||
</script> |
@ -1,84 +0,0 @@ |
|||
<template> |
|||
|
|||
<div class="p-d-flex p-flex-row p-jc-between p-ai-stretch"> |
|||
<Panel |
|||
class="p-mr-2 p-as-stretch file-card" |
|||
> |
|||
<template #header> |
|||
File preview |
|||
</template> |
|||
|
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
</Panel> |
|||
|
|||
<Card class="p-mr-2 p-as-stretch filters-card"> |
|||
<template #header> |
|||
<Toolbar> |
|||
<template #left> |
|||
<h3>Available filters</h3> |
|||
</template> |
|||
<template #right> |
|||
<Button |
|||
icon="pi pi-plus" |
|||
class="p-button-success" |
|||
/> |
|||
</template> |
|||
</Toolbar> |
|||
</template> |
|||
|
|||
<template #content> |
|||
<filter-view |
|||
v-for="(filter, id, index) in filters" |
|||
:key="index" |
|||
:id="id" |
|||
:display-name="filter.display_name" |
|||
:options="filter.options" |
|||
></filter-view> |
|||
</template> |
|||
</Card> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import axios from 'axios'; |
|||
import { Vue, Component, Prop } from 'vue-property-decorator'; |
|||
import { FileData } from '../interfaces/FileData'; |
|||
import { FilterInterface } from '../interfaces/FilterInterface'; |
|||
|
|||
@Component |
|||
export default class ProcessFile extends Vue { |
|||
|
|||
@Prop({ default: null }) |
|||
public readonly file!: FileData|null; |
|||
|
|||
@Prop({ default: [] }) |
|||
public readonly filters!: { [keys:string]: FilterInterface } |
|||
|
|||
private selectedFile: File|null = null; |
|||
private selectedFilters = []; |
|||
|
|||
/** |
|||
* |
|||
*/ |
|||
created() { |
|||
console.log('FILE: ', this.file); |
|||
console.log('FILTERS: ', this.filters); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.file-card { |
|||
flex: 0 1 66%; |
|||
} |
|||
|
|||
.filters-card { |
|||
flex: 0 1 32% |
|||
} |
|||
</style> |
@ -0,0 +1,11 @@ |
|||
.file-card { |
|||
flex: 0 1 74%; |
|||
} |
|||
|
|||
.filters-card { |
|||
flex: 0 1 24% |
|||
} |
|||
.p-overlaypanel { |
|||
// width: 450px; |
|||
min-width: 300px; |
|||
} |
@ -0,0 +1,121 @@ |
|||
import axios from 'axios'; |
|||
// import OverlayPanel from 'primevue/overlaypanel/OverlayPanel';
|
|||
import { Vue, Component, Prop, Watch } from 'vue-property-decorator'; |
|||
import { FileData } from '@interfaces/FileData'; |
|||
|
|||
@Component |
|||
export default class ProcessFile extends Vue { |
|||
|
|||
/** |
|||
* Props |
|||
*/ |
|||
// The data for the file we are processing
|
|||
@Prop({ default: { id: -1, file: '', path: '' } }) |
|||
public readonly file!: FileData; |
|||
|
|||
// The list of available searchers
|
|||
@Prop({ default: [] }) |
|||
public readonly searchers!: { [keys: string]: string; } |
|||
|
|||
|
|||
/** |
|||
* Class members |
|||
*/ |
|||
public $refs!: { |
|||
'searchers-overlay': any |
|||
} |
|||
|
|||
// The id of the interval used to query the file status
|
|||
private intervalId!: any; |
|||
|
|||
// The content of the file we are processing
|
|||
private fileContent: string = ''; |
|||
|
|||
// The list of filters/searchers in a format usable by the datatable
|
|||
private searchersData: Array<{ id: string; name: string; }> = []; |
|||
|
|||
// The list of selected filters/searchers
|
|||
private selectedSearchers: Array<{ id: string; name: string; }> = []; |
|||
|
|||
//The list of expanded rows in the selected filters/searchers table
|
|||
private expandedRows: Array<{id: string; name: string; }> = []; |
|||
|
|||
/** |
|||
* |
|||
*/ |
|||
created() { |
|||
for(let index in this.searchers) { |
|||
let searcherData = { |
|||
id: index, |
|||
name: this.searchers[index] |
|||
}; |
|||
|
|||
this.searchersData.push(searcherData); |
|||
} |
|||
|
|||
|
|||
this.intervalId = setInterval(this.waitForFile, 3000); |
|||
} |
|||
|
|||
/** |
|||
* |
|||
*/ |
|||
private async waitForFile() { |
|||
|
|||
const response = await this.$api.getFileData(this.file.id); |
|||
|
|||
if (response.text !== null && response.ready === true) { |
|||
|
|||
if (response.ingest_status === 'fail') { |
|||
this.$toast.add({ |
|||
severity: 'error', |
|||
summary: 'File error', |
|||
detail: 'THere was an error processing the file in ingest', |
|||
life: 3000 |
|||
}); |
|||
} else { |
|||
this.fileContent = response.documentContent; |
|||
|
|||
this.$toast.add({ |
|||
severity:'success', |
|||
summary: 'File loaded', |
|||
detail: 'The file has been processed by ingest.', |
|||
life: 3000 |
|||
}); |
|||
|
|||
clearInterval(this.intervalId); |
|||
} |
|||
} else { |
|||
console.log('FILE NOT READY YET!'); |
|||
} |
|||
} |
|||
|
|||
private toggleSearchersMenu($event: any) { |
|||
this.$refs['searchers-overlay'].toggle($event); |
|||
} |
|||
|
|||
private onRowSelect($event: any) { |
|||
console.log('SELECT: ', $event); |
|||
console.log(this.selectedSearchers); |
|||
} |
|||
|
|||
private onRowUnselect($event: any) { |
|||
console.log('UNSELECT: ', $event); |
|||
console.log(this.selectedSearchers); |
|||
} |
|||
|
|||
private onSelectedSearchersReorder($event: any) |
|||
{ |
|||
this.selectedSearchers = $event.value; |
|||
} |
|||
|
|||
private onSelectedSearcherExpand($event:any) |
|||
{ |
|||
|
|||
} |
|||
|
|||
private onSelectedSearcherCollapse($event:any) |
|||
{ |
|||
|
|||
} |
|||
} |
@ -0,0 +1,111 @@ |
|||
<template> |
|||
|
|||
<div class="p-d-flex p-flex-row p-jc-between p-ai-stretch"> |
|||
<Toast /> |
|||
<Card |
|||
class="p-mr-2 p-as-stretch file-card" |
|||
> |
|||
<template #header> |
|||
<Toolbar> |
|||
<template #left> |
|||
<h3>File preview</h3> |
|||
</template> |
|||
</Toolbar> |
|||
</template> |
|||
|
|||
<template #content> |
|||
<template v-if="fileContent === ''"> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
<Skeleton /><br /> |
|||
</template> |
|||
<template v-else> |
|||
<vue-markdown :source="fileContent" /> |
|||
</template> |
|||
</template> |
|||
|
|||
</Card> |
|||
|
|||
<Card class="p-mr-2 p-as-stretch filters-card"> |
|||
<template #header> |
|||
<Toolbar> |
|||
<template #left> |
|||
<h3>Document searchers</h3> |
|||
</template> |
|||
<template #right> |
|||
<Button |
|||
icon="pi pi-plus" |
|||
class="p-button-success p-button-sm p-button-text" |
|||
@click="toggleSearchersMenu" |
|||
aria:haspopup="true" |
|||
aria-controls="overlay_panel" /> |
|||
</template> |
|||
</Toolbar> |
|||
</template> |
|||
|
|||
<template #content> |
|||
<DataTable |
|||
:value.sync="selectedSearchers" |
|||
dataKey="id" |
|||
:expandedRows.sync="expandedRows" |
|||
@row-reorder="onSelectedSearchersReorder" |
|||
@row-expand="onSelectedSearcherExpand" |
|||
@row-collapse="onSelectedSearcherCollapse"> |
|||
|
|||
<Column :rowReorder="true" headerStyle="width: 3rem" /> |
|||
<Column field="name" header="Name" sortable></Column> |
|||
<Column :expander="true" headerStyle="width: 3rem" /> |
|||
|
|||
<template #expansion="slotProps"> |
|||
<div class="options-subtable"> |
|||
<!-- TODO: Add real options here --> |
|||
<h5>Options for {{slotProps.data.name}}</h5> |
|||
|
|||
<!-- <ProgressSpinner /> --> |
|||
|
|||
<div class="p-fluid"> |
|||
<div class="p-field"> |
|||
<label for="firstname">Option 1</label> |
|||
<InputText id="firstname" type="text" class="p-inputtext-sm" /> |
|||
</div> |
|||
<div class="p-field"> |
|||
<label for="lastname">Option 2</label> |
|||
<InputText id="lastname" type="text" class="p-inputtext-sm" /> |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
</template> |
|||
|
|||
</DataTable> |
|||
</template> |
|||
</Card> |
|||
|
|||
<OverlayPanel |
|||
ref="searchers-overlay" |
|||
appendTo="body" |
|||
id="overlay_panel" |
|||
style="width: 300px"> |
|||
|
|||
<DataTable |
|||
:value.sync="searchersData" |
|||
:selection.sync="selectedSearchers" |
|||
dataKey="id" |
|||
selectionMode="multiple" |
|||
class="p-datatable-sm" |
|||
:metaKeySelection="false"> |
|||
|
|||
<Column selectionMode="multiple" headerStyle="width: 3em"></Column> |
|||
<Column field="name" header="Name" sortable></Column> |
|||
|
|||
</DataTable> |
|||
</OverlayPanel> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts" src="./ProcessFile.ts"></script> |
|||
<style lang="scss" src="./ProcessFile.scss"></style> |
@ -1,69 +0,0 @@ |
|||
<template> |
|||
<Fieldset :legend="displayName" :toggleable="true" class="filter-container"> |
|||
|
|||
<div v-for="option of optionsList" :key="option.name" class="filter-option"> |
|||
<h5>{{ option.name }}</h5> |
|||
|
|||
<SelectButton |
|||
:key="option.name" |
|||
v-model="selectedOption" |
|||
:options="option.list" |
|||
optionLabel="name" /> |
|||
</div> |
|||
</Fieldset> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
|
|||
import { Vue, Component, Prop } from 'vue-property-decorator'; |
|||
import { FilterOptions } from '../../interfaces/FilterOptions'; |
|||
|
|||
@Component |
|||
export default class Filter extends Vue { |
|||
|
|||
@Prop(String) |
|||
public readonly id!: string; |
|||
|
|||
@Prop(String) |
|||
public readonly displayName!: string; |
|||
|
|||
@Prop({ default: [] }) |
|||
public readonly options!: FilterOptions; |
|||
|
|||
private optionsList = new Array; |
|||
|
|||
public selectedOption = null; |
|||
|
|||
public created() |
|||
{ |
|||
for (let index in this.options) { |
|||
let words = index.split('_'); |
|||
for (let i = 0; i < words.length; i++) { |
|||
words[i] = words[i].charAt(0).toUpperCase() + words[i].substr(1); |
|||
} |
|||
|
|||
|
|||
|
|||
let option = { |
|||
name: words.join(' '), |
|||
list: new Array |
|||
}; |
|||
this.options[index].forEach( opt => { |
|||
option.list.push({ |
|||
'name': opt, |
|||
'value': opt |
|||
}); |
|||
}); |
|||
|
|||
this.optionsList.push(option); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.filter-container, |
|||
.filter-option { |
|||
margin-bottom: 10px; |
|||
} |
|||
</style> |
@ -1,7 +0,0 @@ |
|||
import { FilterOptions } from "./FilterOptions"; |
|||
|
|||
export interface FilterInterface |
|||
{ |
|||
display_name: string; |
|||
options: FilterOptions; |
|||
} |
@ -1,4 +0,0 @@ |
|||
export interface FilterOptions |
|||
{ |
|||
[keys: string]: string[] |
|||
} |
@ -0,0 +1,18 @@ |
|||
import _Vue, { PluginFunction } from 'vue'; |
|||
import ApiService from '@services/ApiService'; |
|||
|
|||
const ApiPlugin = { |
|||
|
|||
install: (Vue: typeof _Vue, options?: any) => { |
|||
|
|||
let apiService = new ApiService(); |
|||
|
|||
Vue.mixin({ |
|||
created() { |
|||
Vue.prototype.$api = apiService; |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
export default ApiPlugin; |
@ -0,0 +1,7 @@ |
|||
import ApiService from '@services/ApiService'; |
|||
|
|||
declare module 'vue/types/vue' { |
|||
interface Vue { |
|||
$api: ApiService; |
|||
} |
|||
} |
@ -0,0 +1,60 @@ |
|||
import axios from 'axios'; |
|||
|
|||
export default class ApiService { |
|||
private readonly baseUrl: string = 'http://core.sandd'; |
|||
|
|||
private readonly apiRoutes = { |
|||
file: '/api/file', |
|||
searchAndDisplace: '/search-and-displace' |
|||
}; |
|||
|
|||
constructor() |
|||
{} |
|||
|
|||
/** |
|||
* Upload a file to the server and return its response. |
|||
* Throws an error if the response wasn't successful |
|||
* |
|||
* @param file |
|||
* @returns |
|||
*/ |
|||
public async uploadFile(file: File) |
|||
{ |
|||
let formData = new FormData(); |
|||
formData.append('file', file); |
|||
|
|||
try { |
|||
let response = await axios.post( |
|||
this.baseUrl + this.apiRoutes.file, |
|||
formData, |
|||
{ |
|||
headers: { |
|||
'Content-Type': 'multipart/form-data' |
|||
} |
|||
} |
|||
) |
|||
|
|||
return response.data; |
|||
} catch (err) { |
|||
throw err; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get data for a file from the server. |
|||
* Throws an error if the response wasn't successful |
|||
* |
|||
* @param fileId |
|||
* @returns |
|||
*/ |
|||
public async getFileData(fileId: string) |
|||
{ |
|||
try { |
|||
let response = await axios.get(this.baseUrl + this.apiRoutes.searchAndDisplace + `/${fileId}`); |
|||
|
|||
return response.data; |
|||
} catch (err) { |
|||
throw err; |
|||
} |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue