Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
9de206d
structure and some functionalality
Ntobe99 Aug 1, 2023
feddf76
grid and controls added
Ntobe99 Aug 2, 2023
20ab5f0
search using id working
Ntobe99 Aug 11, 2023
161dba8
spinner added
Ntobe99 Aug 11, 2023
4db8da4
some changes
Ntobe99 Aug 11, 2023
4379d03
changes to code format
Ntobe99 Aug 14, 2023
1574a99
scrollbar removed and empty array debug
Ntobe99 Aug 14, 2023
d7cc5a7
dyanmic grid and some search functionality
Ntobe99 Aug 17, 2023
68d6fdc
grid has range (0-999999) and some search functionality
Ntobe99 Aug 21, 2023
5590608
stationary firstVal and improved search functionality
Ntobe99 Aug 24, 2023
92c2304
firstpage functionality improved
Ntobe99 Aug 24, 2023
0449b6f
firstpage functionality improved
Ntobe99 Aug 24, 2023
18305ee
mmm
Ntobe99 Aug 25, 2023
07b2e55
mmm
Ntobe99 Aug 25, 2023
46fa399
final
Ntobe99 Aug 25, 2023
120a175
final
Ntobe99 Aug 25, 2023
0250464
formatting added
Ntobe99 Aug 28, 2023
e79a514
formatting added
Ntobe99 Aug 28, 2023
8df21a6
Update app.ts
Ntobe99 Aug 28, 2023
b2c0a70
script tag moved to head tag
Ntobe99 Aug 28, 2023
b16c29c
script tag moved to head tag
Ntobe99 Aug 28, 2023
75c52e0
some changes
Ntobe99 Aug 29, 2023
5c1408b
update
Ntobe99 Aug 29, 2023
ee7b2ca
update
Ntobe99 Aug 29, 2023
07261b4
update
Ntobe99 Aug 29, 2023
5a64f0b
update
Ntobe99 Aug 29, 2023
b92adc3
update
Ntobe99 Aug 29, 2023
e06829b
update
Ntobe99 Aug 29, 2023
b49d5a8
no more any types
Ntobe99 Aug 29, 2023
28f1ff8
no more any types
Ntobe99 Aug 29, 2023
2a2a633
some changes
Ntobe99 Aug 29, 2023
f70d831
files separated
Ntobe99 Aug 30, 2023
1a2ee0d
checking
Ntobe99 Aug 30, 2023
784d208
file separated into smaller parts
Ntobe99 Aug 30, 2023
7b77515
file separated into smaller parts
Ntobe99 Aug 30, 2023
d7367c0
file separated into smaller parts
Ntobe99 Aug 30, 2023
d15a355
first and last page more dynamic
Ntobe99 Aug 31, 2023
0b1bb06
first and last page more dynamic
Ntobe99 Aug 31, 2023
6318826
unnecessary semicolons removed
Ntobe99 Sep 4, 2023
6b095c5
updates
Ntobe99 Sep 4, 2023
3af4520
some of the requsted changes
Ntobe99 Sep 5, 2023
9722222
some of the requsted changes
Ntobe99 Sep 5, 2023
5a6339b
some of the requested changes
Ntobe99 Sep 5, 2023
c81a308
requested changes
Ntobe99 Sep 5, 2023
60d758a
requested changes
Ntobe99 Sep 6, 2023
79b2f6a
requested changes
Ntobe99 Sep 6, 2023
2d11963
updates
Ntobe99 Sep 6, 2023
3f81c01
updates
Ntobe99 Sep 6, 2023
7a1f771
new updates
Ntobe99 Sep 7, 2023
20a4790
new updates
Ntobe99 Sep 7, 2023
105295d
new updates
Ntobe99 Sep 7, 2023
42bca52
some updates
Ntobe99 Sep 7, 2023
57f6a66
some updates
Ntobe99 Sep 8, 2023
f0e6652
some of the requested updates
Ntobe99 Sep 8, 2023
e31e167
some updates to logic
Ntobe99 Sep 11, 2023
432722c
some updates to logic
Ntobe99 Sep 11, 2023
24af382
some updates to logic
Ntobe99 Sep 11, 2023
9311f71
most updates requested done
Ntobe99 Sep 12, 2023
73c6b0b
most updates requested done
Ntobe99 Sep 12, 2023
a0b243f
most updates requested done
Ntobe99 Sep 12, 2023
2d5786b
most updates requested done
Ntobe99 Sep 12, 2023
c73e797
most updates requested done
Ntobe99 Sep 13, 2023
6b5b5b4
new updates with changes requested
Ntobe99 Sep 13, 2023
9214692
most changes done, to-do add more comments and functionality
Ntobe99 Sep 13, 2023
7c5ee28
most changes done, to-do add more comments and functionality.
Ntobe99 Sep 13, 2023
603fc3d
most changes done, to-do add more comments and functionality.
Ntobe99 Sep 14, 2023
04cb27a
most requested changes done
Ntobe99 Sep 14, 2023
a43ea08
most requested changes done
Ntobe99 Sep 14, 2023
7bf7ba5
most requested changes done
Ntobe99 Sep 14, 2023
c5174fc
most requested changes done
Ntobe99 Sep 14, 2023
4eb4f82
most requested changes done
Ntobe99 Sep 14, 2023
5528fd6
most requested changes done
Ntobe99 Sep 14, 2023
7c7ea1a
requested changes done
Ntobe99 Sep 15, 2023
1df95c0
some changes done
Ntobe99 Sep 18, 2023
9518331
some changes done
Ntobe99 Sep 19, 2023
8093c8a
some changes done
Ntobe99 Sep 19, 2023
dc20c08
changes to search and a few
Ntobe99 Sep 21, 2023
6fd8954
changes to search and a few changes
Ntobe99 Sep 21, 2023
5ffd990
removed unnecessary maxrange var
Ntobe99 Sep 21, 2023
8e9d50a
some changes to logic
Ntobe99 Sep 22, 2023
f2b7bf3
some changes to logic
Ntobe99 Sep 22, 2023
138f01c
accounted for zero based indexing
Ntobe99 Sep 26, 2023
e94ac2e
27/09
Ntobe99 Sep 27, 2023
cd397ac
logic updated
Ntobe99 Sep 29, 2023
e2d638d
logic updated
Ntobe99 Sep 29, 2023
27e027a
style updated for width resize
Ntobe99 Sep 29, 2023
126a1f0
resize logic updated
Ntobe99 Sep 29, 2023
03274eb
changes requested done
Ntobe99 Oct 2, 2023
194d261
changes requested done
Ntobe99 Oct 2, 2023
4d61536
changes requested done
Ntobe99 Oct 2, 2023
350c90b
changes requested done for real this time
Ntobe99 Oct 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/node_modules
app.js
app.js.map
ApiData.js
ApiData.js.map
GridTemp.js
GridTemp.js.map
222 changes: 222 additions & 0 deletions ApiData.ts
Comment thread
FritzOnFire marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Interface to define the structure of grid data
interface ColumnName {
name: string;
}
// Interface to define column names
interface GridData {
[key: string]: any;
}
class ApiData {
// Properties to manage data and settings
pageSize: number;
currentPage: number = 1;
data: GridData[] = [];
totalItems: number = 0;
columnNames: ColumnName[] = [];
maxGridHeight: number = 0;
firstVal: number = 0;
lastVal: number = -1;

constructor(pageSize: number) {
this.pageSize = pageSize;
}

/** Initialize method to set up the grid */
async initialize(): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for async, as you are not awaiting or returning something that is not a promise.

Promise.resolve()
.then(() => this.adjustGridHeight())
.then(() => this.recordCount())
.then(() => this.fetchColumns())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't create a Promise.resolve just to chain with another promise :P. And adjustGridHeight does not return a promise, so no need to treat it as such. And please return the promise to allow other function to await on this function in case they need to.

Suggested change
Promise.resolve()
.then(() => this.adjustGridHeight())
.then(() => this.recordCount())
.then(() => this.fetchColumns())
this.adjustGridHeight();
return this.recordCount()
.then(() => this.fetchColumns())

.then(() => this.fetchRecords())
.then(() => this.setupControls())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.then(() => this.setupControls())
.then(() => this.setupControls());

Missing semi-colon

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing ;

}

/**Method to fetch total record count from the server */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed a space, and no need to say that a method is a method

Suggested change
/**Method to fetch total record count from the server */
/** Fetch total record count from the server */

async recordCount(): Promise<void> {
return this.fetchData('http://localhost:2050/recordCount')
.then((response: any) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You specified the return type for fetchData as either a number or string, you can't specify any here.

const totalItems = response;
this.totalItems = totalItems;
})
.catch(() => {
throw ('Failed to fetch record count.');
});
Comment on lines +56 to +59

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, by catching the error here, and more specifically, not throwing an error here, the calling function has no idea something went wrong, and will try and continue executing the rest of your code. This is usually a good thing, as we don't want a front-end app to break just because one part of it is broken.

But when I look at initialize, it eventually calls fetchRecords, and fetchRecords definitely does need totalItems to have a value. I kinda feel that your program should not try and call fetchRecords if recordCount failed. And for that you are going to have to throw an error here.

}

/**fectch column names*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave a space after the /** and before the */

async fetchColumns(): Promise<void> {
return this.fetchData('http://localhost:2050/columns')
.then((response: any) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You specified the return type for fetchData as either a number or string, you can't specify any here.

const res = JSON.parse(response);
this.columnNames = res.map((columnName: any) => ({ name: columnName }));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure you know what the type of columnName is. Or, at least, what it should be for your code to work.

this.data = new Array<GridData>(this.columnNames.length);
})
.catch(() => {
throw ('Failed to fetch columns.');
});
}

/**get records from API for fetch an search functionality*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave a space after the /** and before the */

async fetchAndProcessRecords(from: number, to: number): Promise<GridData[]> {
$('#spinner').show();
$('#grid').hide();

return this.fetchData(`http://localhost:2050/records?from=${from}&to=${to}`)
.then((response: any) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You specified the return type for fetchData as either a number or string, you can't specify any here.

const res = JSON.parse(response);
const processedData = res.map((record: any) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure you know what the type of record is. Or at least, what it should be for your code to work.

const obj: GridData = {};
for (let j = 0; j < this.columnNames.length && j < record.length; j++) {
obj[this.columnNames[j].name] = record[j];
}
return obj;
});
$('#spinner').hide();
$('#grid').show();
return processedData;
})
.catch(() => {
throw ('Failed to fetch records');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.catch(() => {
throw ('Failed to fetch records');
.catch(error => {
throw ('Failed to fetch records: ', error);

I mean, wouldn't you like to know the error? We know if failed but why?

});
}

/**fetch records from api*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave a space after the /** and before the */

async fetchRecords(): Promise<void> {
const maxRange = this.totalItems - 1;

@CelesteNaude CelesteNaude Sep 11, 2023

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxRange is being set in a lot of places to maxRange = this.totalItems - 1. I think it's worth it to set this value once (where you get the record count) as a class variable.

const from = this.firstVal;
let to = Math.min(from + this.pageSize, maxRange);

if (to >= maxRange) {
// Set currentPage to the last page
this.currentPage = Math.floor(maxRange / this.pageSize) + 1;
to = maxRange;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you not also update the value of from? And I guess firstVal? If not in this if, at least somewhere in the function. Otherwise you are not fetching enough records.


return this.fetchAndProcessRecords(from, to)
.then((processedData) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either remove the () around processedData or specify a type.

this.data = processedData;
this.displayRecords();
this.updatePageInfo();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this.displayRecords() calls this.updatePageInfo() and then we call it again?

Sounds like double work to me 🥵

})
.catch(() => {
throw ('Failed to fetch records');
});
}


/**funtion to search through records using fromID*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave a space after the /** and before the */. Also no need to call a function a function.

async searchRecords(searchValue: number): Promise<void> {
// Maximum allowed Value
const maxRange = this.totalItems - 1;

if (searchValue >= 0 && searchValue <= maxRange) {
const from = searchValue;
const to = Math.min(from + this.pageSize, maxRange);

return this.fetchAndProcessRecords(from, to)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please adjust your from to allow fetching from the back-end as many records as you need to display. As currently this looks like it could only return 2 records when you might need 40 to fill up the grid.

.then((processedData) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either remove the () around processedData or specify a type.

this.data = processedData;
this.currentPage = Math.ceil(from / this.pageSize) + 1;
// Set firstVal to searched value
this.firstVal = from;
// Calculate lastVal based on pageSize
this.lastVal = from + this.pageSize;
this.displayRecords();
this.updatePageInfo();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same vibe as previously mentioned

})
.catch(() => {
throw ('Failed to search value');
});
} else {
alert('Please enter values in the range (0-999999)');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all back-ends will have the same max id. You will have to change this to not be hard coded.

return Promise.resolve();
}
}


//change grid height according to screen size
private adjustGridHeight(): void {
const gridElement = document.getElementById('grid');
const pageCntrl = $('.grid-controls').innerHeight();
const screenHeight = $(window).innerHeight();
if (gridElement && pageCntrl !== undefined && screenHeight !== undefined) {
this.maxGridHeight = screenHeight - pageCntrl;
gridElement.style.height = `${this.maxGridHeight}px`;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this one ;

}

// Update the page information and records display based on the current state of the grid.
private updatePageInfo(): void {
const totalPages = Math.ceil(this.totalItems / this.pageSize);
const pageInfo = `Page ${this.currentPage} of ${totalPages}`;
const maxRange = this.totalItems - 1;
const from = this.firstVal;
let to = Math.min(from + this.pageSize, maxRange);
$('#pageInfo').text(`${pageInfo}`);
$('.records').text(`Showing records ${from} to ${to}`);
}

// use Ajax for data fetching
private async fetchData(url: string): Promise<number | string> {
try {
$('#overlay').show();
const response = await $.ajax({
url,
method: 'GET',
});
$('#overlay').hide();
return response;
} catch (error) {
throw error;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this one ;

}

private setupControls(): void {
$('#prevBtn').on('click', () => this.handlePageChange(-1));
$('#nextBtn').on('click', () => this.handlePageChange(1));
$(window).on('resize', debounce(this.handleResize, 100));
}

private handlePageChange(delta: number): void {
Comment thread
CelesteNaude marked this conversation as resolved.

if (delta > 0 && this.firstVal + delta * this.pageSize > this.totalItems - 1) {
this.firstVal = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help me if I am wrong here... but

  • if delta is positive, so next page.
  • and the current firstVal plus the amount of records you need to show is more than the max
  • You reset firstVal ALL they way back to zero...

This does not sound correct. Should you not be setting it to max - the amount of records you need to show?

} else if (delta < 0 && this.firstVal + delta * this.pageSize < 0) {
this.firstVal = Math.max(0, this.totalItems - this.pageSize);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help me if I am wrong here... but

  • if delta is negative, so previous page.
  • and the current firstVal minus the amount of records you need to show is less than zero
  • You set firstVal to the highest possible value it can be...

This does not sound correct. Should you not be setting it to zero?

} else {
this.firstVal = Math.max(0, Math.min(this.firstVal + delta * this.pageSize, this.totalItems - 1));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, very clean way of ensuring you have a valid value :)

}

this.lastVal = this.firstVal + this.pageSize - 1;
this.currentPage = Math.floor(this.firstVal / this.pageSize) + 1;
this.fetchRecords();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRecords can throw an error, you need to catch it and handle it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to implement error handling here if I have implemented it within the fetchRecords function?

this.updatePageInfo();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if you only did updatePageInfo after fetchRecords has finished. Just in case it fails.

}

private handleResize = (): void => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't make variables on a class that are just functions if you have no intention of changing the value, or allowing someone else to change the value.

const newGridSize = Math.floor((Math.floor($(window).innerHeight() as number) * GRID_RATIO) / ROW_HEIGHT) - 1;

// Check if the new grid size is non-negative
if (newGridSize >= 0) {
// Adjust firstVal for the last page
if (this.firstVal + newGridSize > this.totalItems) {
this.firstVal = Math.max(this.totalItems - newGridSize);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the max here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you not also need -1 here?

}

this.pageSize = newGridSize;
this.lastVal = this.firstVal + newGridSize - 1;

// Fetch records, update page info, and adjust grid height
this.fetchRecords();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRecords can throw an error. You need to catch it and handle it.

this.updatePageInfo();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if you only did updatePageInfo after fetchRecords has finished. Just in case it fails.

this.adjustGridHeight();
Comment thread
FritzOnFire marked this conversation as resolved.
}
}


private displayRecords = (): void => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't make variables on a class that are just functions if you have no intention of changing the value, or allowing someone else to change the value.

const gridTemplate = new GridTemplate(this.columnNames, this.data);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we are creating the GridTemplate class everytime we displayRecords()?

I would have loved that we could just set the dataRecords every time we update the display because columnNames never changes throughout this life. It just sounds like a lot of objects being created and created and created...

gridTemplate.displayRecords();
this.updatePageInfo();
}
}
42 changes: 42 additions & 0 deletions GridTemp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Class to manage the grid template and display records

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change this function to the javadoc format

class GridTemplate {
private columnNames: ColumnName[] = [];
private dataRecords: GridData[] = [];
// Initializes the column names and data records that will be used to display records in the grid.
constructor(columnNames: ColumnName[], dataRecords: GridData[]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an empty line between your constructor and your variables.

this.columnNames = columnNames;
this.dataRecords = dataRecords;
}
// Display records in a grid in table format
displayRecords(): void {

const gridElement = document.getElementById('grid');
if (gridElement) {
gridElement.innerHTML = '';
const table = document.createElement('table');
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
for (const column of this.columnNames) {
const th = document.createElement('th');
th.textContent = column.name;
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
table.appendChild(thead);
// Create table body
const tbody = document.createElement('tbody');
for (const row of this.dataRecords) {
const tr = document.createElement('tr');
for (const column of this.columnNames) {
const td = document.createElement('td');
td.textContent = row[column.name];
tr.appendChild(td);
}
tbody.appendChild(tr);
}
table.appendChild(tbody);
// Append the table to the grid element
gridElement.appendChild(table);
}
}
}
85 changes: 85 additions & 0 deletions Styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
body,
html {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
font-family: system-ui;
}

#overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(231, 238, 238, 0.815);
z-index: 900;
display: none;
}

.spinner-container {
text-align: center;
}

#grid {
border: 1px solid #333333;
text-align: center;
background-color: #f2f6f7;
}

/* table */
table {
height: 100%;
border-collapse: collapse;
}

tr {
border: 1px solid #333333;
}

thead {
background-color: #333333;
font-weight: 450;
color: white;
}

td {
padding: 2px;
border: 1px solid #333333;
align-content: center;
width: 350px;
}

.grid-controls {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
color: white;
background-color: #333333;
position: sticky;
bottom: 0;
}

#pageInfo,
.records {
font-weight: 450;
background-color: #333333;
}

#fromInput {
padding: 5px 10px;
margin: 0 5px;
background-color: #232425;
color: white;
}

/* button */
.btn {
cursor: pointer;
padding: 5px 10px;
margin: 0 5px;
color: white;
background-color: #5a5f61;
}
Loading