panther 4.3.7__py3-none-any.whl → 5.0.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- panther/__init__.py +1 -1
- panther/_load_configs.py +78 -64
- panther/_utils.py +1 -1
- panther/app.py +126 -60
- panther/authentications.py +26 -9
- panther/base_request.py +27 -2
- panther/base_websocket.py +26 -27
- panther/cli/create_command.py +1 -0
- panther/cli/main.py +19 -27
- panther/cli/monitor_command.py +8 -4
- panther/cli/template.py +11 -6
- panther/cli/utils.py +3 -2
- panther/configs.py +7 -9
- panther/db/cursor.py +23 -7
- panther/db/models.py +26 -19
- panther/db/queries/base_queries.py +1 -1
- panther/db/queries/mongodb_queries.py +177 -13
- panther/db/queries/pantherdb_queries.py +5 -5
- panther/db/queries/queries.py +1 -1
- panther/events.py +10 -4
- panther/exceptions.py +24 -2
- panther/generics.py +2 -2
- panther/main.py +90 -117
- panther/middlewares/__init__.py +1 -1
- panther/middlewares/base.py +15 -19
- panther/middlewares/monitoring.py +42 -0
- panther/openapi/__init__.py +1 -0
- panther/openapi/templates/openapi.html +27 -0
- panther/openapi/urls.py +5 -0
- panther/openapi/utils.py +167 -0
- panther/openapi/views.py +101 -0
- panther/pagination.py +1 -1
- panther/panel/middlewares.py +10 -0
- panther/panel/templates/base.html +14 -0
- panther/panel/templates/create.html +21 -0
- panther/panel/templates/create.js +1270 -0
- panther/panel/templates/detail.html +55 -0
- panther/panel/templates/home.html +9 -0
- panther/panel/templates/home.js +30 -0
- panther/panel/templates/login.html +47 -0
- panther/panel/templates/sidebar.html +13 -0
- panther/panel/templates/table.html +73 -0
- panther/panel/templates/table.js +339 -0
- panther/panel/urls.py +10 -5
- panther/panel/utils.py +98 -0
- panther/panel/views.py +143 -0
- panther/request.py +3 -0
- panther/response.py +91 -53
- panther/routings.py +7 -2
- panther/serializer.py +1 -1
- panther/utils.py +34 -26
- panther/websocket.py +3 -0
- {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/METADATA +19 -17
- panther-5.0.0b2.dist-info/RECORD +75 -0
- {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/WHEEL +1 -1
- panther-4.3.7.dist-info/RECORD +0 -57
- {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/entry_points.txt +0 -0
- {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/licenses/LICENSE +0 -0
- {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
{% extends "base.html" %} {% block content %}
|
2
|
+
<div class="container mx-auto px-4 py-8">
|
3
|
+
<div class="flex justify-between items-center mb-8">
|
4
|
+
<h1 class="text-2xl font-bold">Update Record</h1>
|
5
|
+
<button
|
6
|
+
onclick="redirectToTable()"
|
7
|
+
class="px-4 py-2 bg-gray-800 rounded hover:bg-gray-700"
|
8
|
+
>
|
9
|
+
Back to List
|
10
|
+
</button>
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<form id="updateForm" class="mt-6">
|
14
|
+
<div
|
15
|
+
id="dynamicInputs"
|
16
|
+
class="bg-gray-800 border-gray-400 rounded-lg p-6 shadow-lg space-y-4"
|
17
|
+
></div>
|
18
|
+
<div class="flex justify-start mt-6 space-x-4">
|
19
|
+
<button
|
20
|
+
type="submit"
|
21
|
+
class="px-6 py-2 bg-blue-600 rounded hover:bg-blue-700 text-white"
|
22
|
+
>
|
23
|
+
Update
|
24
|
+
</button>
|
25
|
+
<button
|
26
|
+
type="button"
|
27
|
+
id="deleteButton"
|
28
|
+
class="px-6 py-2 bg-red-600 rounded hover:bg-red-700 text-white"
|
29
|
+
>
|
30
|
+
Delete
|
31
|
+
</button>
|
32
|
+
</div>
|
33
|
+
</form>
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<script>
|
37
|
+
const data = JSON.parse(`{{data|tojson|safe}}`);
|
38
|
+
console.log("Data passed to the form:", data); // Debugging log
|
39
|
+
const isUpdate = true;
|
40
|
+
const existingData = data;
|
41
|
+
function redirectToTable() {
|
42
|
+
const currentUrl = window.location.pathname;
|
43
|
+
const urlParts = currentUrl.split("/").filter((part) => part !== "");
|
44
|
+
urlParts.pop();
|
45
|
+
const redirectUrl = "/" + urlParts.join("/") + "/";
|
46
|
+
|
47
|
+
console.log("Redirecting to:", redirectUrl);
|
48
|
+
|
49
|
+
window.location.href = redirectUrl;
|
50
|
+
|
51
|
+
}
|
52
|
+
{% include "create.js" %}
|
53
|
+
|
54
|
+
</script>
|
55
|
+
{% endblock %}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
function getCurrentTableIndex() {
|
2
|
+
const path = window.location.pathname;
|
3
|
+
const match = path.match(/\/admin\/(\d+)/);
|
4
|
+
return match ? parseInt(match[1]) : 0;
|
5
|
+
}
|
6
|
+
|
7
|
+
function selectTable(element) {
|
8
|
+
const index = element.dataset.index;
|
9
|
+
// Always trigger a page reload even if it's the same index
|
10
|
+
window.location.href = `/admin/${index}/`;
|
11
|
+
}
|
12
|
+
|
13
|
+
function setActiveTableFromUrl() {
|
14
|
+
const currentIndex = getCurrentTableIndex();
|
15
|
+
const tableItems = document.querySelectorAll('.table-item');
|
16
|
+
|
17
|
+
tableItems.forEach((item) => {
|
18
|
+
item.classList.remove('bg-blue-600');
|
19
|
+
item.classList.add('bg-gray-700');
|
20
|
+
|
21
|
+
if (parseInt(item.dataset.index) === currentIndex) {
|
22
|
+
item.classList.remove('bg-gray-700');
|
23
|
+
item.classList.add('bg-blue-600');
|
24
|
+
}
|
25
|
+
});
|
26
|
+
}
|
27
|
+
|
28
|
+
document.addEventListener('DOMContentLoaded', () => {
|
29
|
+
setActiveTableFromUrl();
|
30
|
+
});
|
@@ -0,0 +1,47 @@
|
|
1
|
+
{% extends "base.html" %}
|
2
|
+
|
3
|
+
{% block content %}
|
4
|
+
<div class="flex items-center justify-center min-h-screen">
|
5
|
+
<div class="bg-gray-800 p-6 rounded-lg shadow-md w-full max-w-sm border border-gray-700">
|
6
|
+
<h2 class="text-xl font-semibold mb-6 text-center">Login</h2>
|
7
|
+
|
8
|
+
{% if error %}
|
9
|
+
<div class="mb-4 p-3 text-sm text-red-600 bg-red-100 border border-red-400 rounded-lg">
|
10
|
+
{{ error }}
|
11
|
+
</div>
|
12
|
+
{% endif %}
|
13
|
+
|
14
|
+
<form action="" method="POST" class="space-y-4">
|
15
|
+
<!-- Username Field -->
|
16
|
+
<div>
|
17
|
+
<label for="username" class="block text-sm font-medium mb-2">Username:</label>
|
18
|
+
<input
|
19
|
+
type="text"
|
20
|
+
id="username"
|
21
|
+
name="username"
|
22
|
+
required
|
23
|
+
class="w-full p-2 bg-gray-700 rounded-lg border border-gray-600 text-white focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
24
|
+
/>
|
25
|
+
</div>
|
26
|
+
<!-- Password Field -->
|
27
|
+
<div>
|
28
|
+
<label for="password" class="block text-sm font-medium mb-2">Password:</label>
|
29
|
+
<input
|
30
|
+
type="password"
|
31
|
+
id="password"
|
32
|
+
name="password"
|
33
|
+
required
|
34
|
+
class="w-full p-2 bg-gray-700 rounded-lg border border-gray-600 text-white focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
35
|
+
/>
|
36
|
+
</div>
|
37
|
+
<!-- Submit Button -->
|
38
|
+
<button
|
39
|
+
type="submit"
|
40
|
+
class="w-full bg-blue-600 hover:bg-blue-500 text-white py-2 px-4 rounded-lg font-semibold transition-colors"
|
41
|
+
>
|
42
|
+
Login
|
43
|
+
</button>
|
44
|
+
</form>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
{% endblock %}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
|
2
|
+
<aside class="w-64 bg-gray-800 p-4 border-gray-700 shrink-0 rounded">
|
3
|
+
<h2 class="text-lg font-semibold mb-4">Available Tables</h2>
|
4
|
+
<div class="space-y-2">
|
5
|
+
{% for table in tables %}
|
6
|
+
<div class="table-item p-3 rounded-lg bg-gray-700 hover:bg-gray-600 cursor-pointer transition-colors"
|
7
|
+
data-index="{{loop.index0}}" onclick="selectTable(this)">
|
8
|
+
<div class="text-sm font-medium">{{table.name}}</div>
|
9
|
+
<div class="text-xs text-gray-400">{{table.module}}</div>
|
10
|
+
</div>
|
11
|
+
{% endfor %}
|
12
|
+
</div>
|
13
|
+
</aside>
|
@@ -0,0 +1,73 @@
|
|
1
|
+
{% extends "base.html" %} {% block content %}
|
2
|
+
<div class="flex items-start gap-4">
|
3
|
+
<aside class="w-64 bg-gray-800 p-4 border-gray-700 shrink-0 rounded">
|
4
|
+
<h2 class="text-lg font-semibold mb-4">Available Tables</h2>
|
5
|
+
<div class="space-y-2">
|
6
|
+
{% for table in tables %}
|
7
|
+
<div
|
8
|
+
class="table-item p-3 rounded-lg bg-gray-700 hover:bg-gray-600 cursor-pointer transition-colors flex justify-between items-center"
|
9
|
+
data-index="{{loop.index0}}"
|
10
|
+
onclick="selectTable(this, event)"
|
11
|
+
>
|
12
|
+
<div>
|
13
|
+
<div class="text-sm font-medium">{{table.name}}</div>
|
14
|
+
<div class="text-xs text-gray-400">{{table.module}}</div>
|
15
|
+
</div>
|
16
|
+
<button
|
17
|
+
class="add-record-btn hidden p-2 text-xs flex bg-green-600 text-white rounded-full hover:bg-green-700 transition-colors duration-200"
|
18
|
+
onclick="goToCreatePage(event)"
|
19
|
+
title="Create new record"
|
20
|
+
>
|
21
|
+
Add
|
22
|
+
<svg
|
23
|
+
class="w-4 h-4"
|
24
|
+
fill="none"
|
25
|
+
stroke="currentColor"
|
26
|
+
viewBox="0 0 24 24"
|
27
|
+
xmlns="http://www.w3.org/2000/svg"
|
28
|
+
>
|
29
|
+
<path
|
30
|
+
stroke-linecap="round"
|
31
|
+
stroke-linejoin="round"
|
32
|
+
stroke-width="2"
|
33
|
+
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
34
|
+
></path>
|
35
|
+
</svg>
|
36
|
+
</button>
|
37
|
+
</div>
|
38
|
+
{% endfor %}
|
39
|
+
</div>
|
40
|
+
|
41
|
+
</aside>
|
42
|
+
|
43
|
+
<div>
|
44
|
+
<table class="w-full bg-gray-800 rounded overflow-hidden">
|
45
|
+
<thead class="bg-gray-700" id="tableHead"></thead>
|
46
|
+
<tbody id="tableBody">
|
47
|
+
{% for record in records %}
|
48
|
+
<tr class="record-row border-t border-gray-700 hover:bg-gray-750">
|
49
|
+
<td class="record-data hidden">{{record}}</td>
|
50
|
+
</tr>
|
51
|
+
{% endfor %}
|
52
|
+
</tbody>
|
53
|
+
<tfoot
|
54
|
+
id="paginationControls"
|
55
|
+
class="my-4 flex-grow flex mx-[100%] gap-4 justify-center w-full min-w-80"
|
56
|
+
></tfoot>
|
57
|
+
</table>
|
58
|
+
</div>
|
59
|
+
</div>
|
60
|
+
<style>
|
61
|
+
details[open] summary svg {
|
62
|
+
transform: rotate(180deg);
|
63
|
+
}
|
64
|
+
|
65
|
+
details summary::-webkit-details-marker {
|
66
|
+
display: none;
|
67
|
+
}
|
68
|
+
</style>
|
69
|
+
<script>
|
70
|
+
{% include "table.js" %}
|
71
|
+
</script>
|
72
|
+
|
73
|
+
{% endblock %}
|
@@ -0,0 +1,339 @@
|
|
1
|
+
const fields = JSON.parse(`{{fields|tojson|safe}}`);
|
2
|
+
const fieldsObject = fields.fields;
|
3
|
+
const fieldsArray = Object.entries(fieldsObject).map(([key, value]) => ({
|
4
|
+
title: value.title || key,
|
5
|
+
type: value.type || [],
|
6
|
+
required: value.required || false,
|
7
|
+
}));
|
8
|
+
|
9
|
+
function pythonToJSON(str) {
|
10
|
+
str = str.replace(/datetime\.datetime\(([^)]+)\)/g, (match, contents) => {
|
11
|
+
const parts = contents.split(",").map((part) => part.trim());
|
12
|
+
if (parts.length >= 6) {
|
13
|
+
return `"${parts[0]}-${parts[1].padStart(2, "0")}-${parts[2].padStart(
|
14
|
+
2,
|
15
|
+
"0"
|
16
|
+
)} ${parts[3].padStart(2, "0")}:${parts[4].padStart(
|
17
|
+
2,
|
18
|
+
"0"
|
19
|
+
)}:${parts[5].padStart(2, "0")}"`;
|
20
|
+
}
|
21
|
+
return '"Invalid datetime"';
|
22
|
+
});
|
23
|
+
|
24
|
+
str = str.replace(/tzinfo=\)/g, "tzinfo=None)");
|
25
|
+
return str
|
26
|
+
.replace(/'/g, '"')
|
27
|
+
.replace(/False/g, "false")
|
28
|
+
.replace(/True/g, "true")
|
29
|
+
.replace(/None/g, "null");
|
30
|
+
}
|
31
|
+
function goToCreatePage() {
|
32
|
+
// Get the current URL without any trailing slash
|
33
|
+
const currentUrl = window.location.href.replace(/\/+$/, "");
|
34
|
+
// Navigate to the current URL + /create
|
35
|
+
window.location.href = `${currentUrl}/create`;
|
36
|
+
}
|
37
|
+
|
38
|
+
function getDataType(value) {
|
39
|
+
if (value === null) return "null";
|
40
|
+
if (Array.isArray(value)) return "array";
|
41
|
+
return typeof value;
|
42
|
+
}
|
43
|
+
|
44
|
+
let allRecords = [];
|
45
|
+
function initializeRecords() {
|
46
|
+
if (allRecords.length === 0) {
|
47
|
+
try {
|
48
|
+
allRecords = Array.from(document.querySelectorAll(".record-data")).map(
|
49
|
+
(item) => {
|
50
|
+
try {
|
51
|
+
return JSON.parse(pythonToJSON(item.textContent.trim()));
|
52
|
+
} catch (e) {
|
53
|
+
console.error("Error parsing record:", e);
|
54
|
+
return {};
|
55
|
+
}
|
56
|
+
}
|
57
|
+
);
|
58
|
+
} catch (e) {
|
59
|
+
console.error("Error initializing records:", e);
|
60
|
+
allRecords = [];
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
function formatValue(value, type) {
|
66
|
+
if (value === null) return '<span class="text-gray-400">null</span>';
|
67
|
+
|
68
|
+
switch (type) {
|
69
|
+
case "array":
|
70
|
+
return `
|
71
|
+
<div class="w-full">
|
72
|
+
<details class="bg-gray-700 rounded-lg max-w-[300px]">
|
73
|
+
<summary class="cursor-pointer px-4 py-2 text-sm font-medium text-gray-300 hover:bg-gray-600 rounded-lg flex items-center justify-between">
|
74
|
+
<span>Array (${value.length} items)</span>
|
75
|
+
<svg class="w-4 h-4 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
76
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
77
|
+
</svg>
|
78
|
+
</summary>
|
79
|
+
<div class="px-4 py-2 border-t border-gray-600">
|
80
|
+
${value
|
81
|
+
.map(
|
82
|
+
(item, index) => `
|
83
|
+
<div class="py-1 text-sm text-gray-300">
|
84
|
+
<span class="text-gray-400">[${index}]:</span>
|
85
|
+
${formatValue(item, getDataType(item))}
|
86
|
+
</div>
|
87
|
+
`
|
88
|
+
)
|
89
|
+
.join("")}
|
90
|
+
</div>
|
91
|
+
</details>
|
92
|
+
</div>
|
93
|
+
`;
|
94
|
+
case "object":
|
95
|
+
if (value === null) {
|
96
|
+
return '<span class="text-gray-400">null</span>';
|
97
|
+
}
|
98
|
+
return `
|
99
|
+
<div class="w-full max-w-[300px]">
|
100
|
+
<details class="bg-gray-700 rounded-lg">
|
101
|
+
<summary class="cursor-pointer px-4 py-2 text-sm font-medium text-gray-300 hover:bg-gray-600 rounded-lg flex items-center justify-between">
|
102
|
+
<span>Object (${Object.keys(value).length} props)</span>
|
103
|
+
<svg class="w-4 h-4 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
104
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
105
|
+
</svg>
|
106
|
+
</summary>
|
107
|
+
<div class="px-4 py-2 border-t border-gray-600">
|
108
|
+
${Object.entries(value)
|
109
|
+
.map(
|
110
|
+
([key, val]) => `
|
111
|
+
<div class="py-1 text-sm text-gray-300">
|
112
|
+
<span class="text-gray-400">${key}:</span>
|
113
|
+
${formatValue(val, getDataType(val))}
|
114
|
+
</div>
|
115
|
+
`
|
116
|
+
)
|
117
|
+
.join("")}
|
118
|
+
</div>
|
119
|
+
</details>
|
120
|
+
</div>
|
121
|
+
`;
|
122
|
+
case "boolean":
|
123
|
+
return `<span class="px-2 py-1 rounded ${
|
124
|
+
value ? "bg-green-500/20 text-green-400" : "bg-red-500/20 text-red-400"
|
125
|
+
}">${value}</span>`;
|
126
|
+
default:
|
127
|
+
return `<span class="text-sm ${
|
128
|
+
typeof value === "string" ? "font-mono" : ""
|
129
|
+
}">${String(value)}</span>`;
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
function toggleDropdown(button) {
|
134
|
+
const dropdown = button.nextElementSibling;
|
135
|
+
const allDropdowns = document.querySelectorAll(
|
136
|
+
".relative.inline-block .absolute"
|
137
|
+
);
|
138
|
+
|
139
|
+
// Close all other dropdowns
|
140
|
+
allDropdowns.forEach((d) => {
|
141
|
+
if (d !== dropdown) d.classList.add("hidden");
|
142
|
+
});
|
143
|
+
|
144
|
+
// Toggle current dropdown
|
145
|
+
dropdown.classList.toggle("hidden");
|
146
|
+
|
147
|
+
// Close dropdown when clicking outside
|
148
|
+
const closeDropdown = (e) => {
|
149
|
+
if (!button.contains(e.target) && !dropdown.contains(e.target)) {
|
150
|
+
dropdown.classList.add("hidden");
|
151
|
+
document.removeEventListener("click", closeDropdown);
|
152
|
+
}
|
153
|
+
};
|
154
|
+
|
155
|
+
document.addEventListener("click", closeDropdown);
|
156
|
+
}
|
157
|
+
|
158
|
+
// Define pagination variables
|
159
|
+
let currentPage = 1;
|
160
|
+
const rowsPerPage = 10; // Number of rows per page
|
161
|
+
|
162
|
+
function renderTable() {
|
163
|
+
// Initialize records if not already done
|
164
|
+
initializeRecords();
|
165
|
+
|
166
|
+
const thead = document.getElementById("tableHead");
|
167
|
+
// Use the global records array instead of re-parsing
|
168
|
+
const records = allRecords;
|
169
|
+
console.log(records);
|
170
|
+
if (records.length > 0) {
|
171
|
+
const firstRecord = records[0];
|
172
|
+
const headers = Object.entries(firstRecord).map(([key, value]) => ({
|
173
|
+
key,
|
174
|
+
type: getDataType(value),
|
175
|
+
}));
|
176
|
+
// Render table headers
|
177
|
+
thead.innerHTML = `
|
178
|
+
<tr>
|
179
|
+
${headers
|
180
|
+
.map(
|
181
|
+
(field) => `
|
182
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">
|
183
|
+
${field.key} (${field.type})
|
184
|
+
</th>
|
185
|
+
`
|
186
|
+
)
|
187
|
+
.join("")}
|
188
|
+
</tr>
|
189
|
+
`;
|
190
|
+
|
191
|
+
// Calculate start and end indices for the current page
|
192
|
+
const startIndex = (currentPage - 1) * rowsPerPage;
|
193
|
+
const endIndex = startIndex + rowsPerPage;
|
194
|
+
|
195
|
+
// Get the rows for the current page
|
196
|
+
const rowsForPage = records.slice(startIndex, endIndex);
|
197
|
+
|
198
|
+
// Get all row elements
|
199
|
+
const rowElements = document.querySelectorAll(".record-row");
|
200
|
+
|
201
|
+
// Render table rows
|
202
|
+
rowElements.forEach((row, index) => {
|
203
|
+
const record = rowsForPage[index];
|
204
|
+
if (record) {
|
205
|
+
row.innerHTML = headers
|
206
|
+
.map(({ key }) => {
|
207
|
+
const cellValue = record[key];
|
208
|
+
const cellType = getDataType(cellValue);
|
209
|
+
|
210
|
+
// If this is the ID cell, make it clickable
|
211
|
+
if (key === "id") {
|
212
|
+
return `
|
213
|
+
<td class="px-6 py-4">
|
214
|
+
<a href="${window.location.href.replace(
|
215
|
+
/\/$/,
|
216
|
+
""
|
217
|
+
)}/${cellValue}"
|
218
|
+
class="text-blue-400 hover:text-blue-300 hover:underline cursor-pointer">
|
219
|
+
${formatValue(cellValue, cellType)}
|
220
|
+
</a>
|
221
|
+
</td>
|
222
|
+
`;
|
223
|
+
} else {
|
224
|
+
return `
|
225
|
+
<td class="px-6 py-4">
|
226
|
+
${formatValue(cellValue, cellType)}
|
227
|
+
</td>
|
228
|
+
`;
|
229
|
+
}
|
230
|
+
})
|
231
|
+
.join("");
|
232
|
+
row.style.display = ""; // Show the row
|
233
|
+
|
234
|
+
// Remove the row click event
|
235
|
+
row.style.cursor = "default";
|
236
|
+
row.onclick = null;
|
237
|
+
} else {
|
238
|
+
row.style.display = "none"; // Hide unused rows
|
239
|
+
}
|
240
|
+
});
|
241
|
+
|
242
|
+
// Render pagination controls
|
243
|
+
renderPaginationControls(records.length);
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
function renderPaginationControls(totalRows) {
|
248
|
+
const paginationContainer = document.getElementById("paginationControls");
|
249
|
+
const totalPages = Math.ceil(totalRows / rowsPerPage);
|
250
|
+
|
251
|
+
// Render "Previous" and "Next" buttons
|
252
|
+
paginationContainer.innerHTML = `
|
253
|
+
<button
|
254
|
+
class="px-4 py-2 bg-gray-600 text-white rounded mr-2"
|
255
|
+
onclick="changePage('prev')"
|
256
|
+
${currentPage === 1 ? "disabled" : ""}>
|
257
|
+
Previous
|
258
|
+
</button>
|
259
|
+
<span class="text-gray-300 pt-1">Page ${currentPage} of ${totalPages}</span>
|
260
|
+
<button
|
261
|
+
class="px-4 py-2 bg-gray-600 text-white rounded ml-2"
|
262
|
+
onclick="changePage('next')"
|
263
|
+
${currentPage === totalPages ? "disabled" : ""}>
|
264
|
+
Next
|
265
|
+
</button>
|
266
|
+
`;
|
267
|
+
}
|
268
|
+
|
269
|
+
function changePage(direction) {
|
270
|
+
const totalPages = Math.ceil(allRecords.length / rowsPerPage);
|
271
|
+
|
272
|
+
if (direction === "prev" && currentPage > 1) {
|
273
|
+
currentPage--;
|
274
|
+
} else if (direction === "next" && currentPage < totalPages) {
|
275
|
+
currentPage++;
|
276
|
+
}
|
277
|
+
|
278
|
+
renderTable();
|
279
|
+
}
|
280
|
+
|
281
|
+
function getCurrentTableIndex() {
|
282
|
+
const path = window.location.pathname;
|
283
|
+
const match = path.match(/\/admin\/(\d+)/);
|
284
|
+
return match ? parseInt(match[1]) : 0;
|
285
|
+
}
|
286
|
+
|
287
|
+
function selectTable(element, event) {
|
288
|
+
const index = element.dataset.index;
|
289
|
+
|
290
|
+
// Hide all add buttons first
|
291
|
+
document.querySelectorAll('.add-record-btn').forEach(btn => {
|
292
|
+
btn.classList.add('hidden');
|
293
|
+
});
|
294
|
+
|
295
|
+
// Show the add button for the clicked table
|
296
|
+
const addButton = element.querySelector('.add-record-btn');
|
297
|
+
if (addButton) {
|
298
|
+
addButton.classList.remove('hidden');
|
299
|
+
}
|
300
|
+
|
301
|
+
// Only update URL if it's different from current
|
302
|
+
if (index !== getCurrentTableIndex().toString()) {
|
303
|
+
window.location.href = `/admin/${index}`;
|
304
|
+
}
|
305
|
+
}
|
306
|
+
|
307
|
+
// Set active table based on URL
|
308
|
+
function setActiveTableFromUrl() {
|
309
|
+
const currentIndex = getCurrentTableIndex();
|
310
|
+
const tableItems = document.querySelectorAll(".table-item");
|
311
|
+
|
312
|
+
tableItems.forEach((item) => {
|
313
|
+
// Hide all add buttons first
|
314
|
+
const addButton = item.querySelector('.add-record-btn');
|
315
|
+
if (addButton) {
|
316
|
+
addButton.classList.add('hidden');
|
317
|
+
}
|
318
|
+
|
319
|
+
item.classList.remove("bg-blue-800");
|
320
|
+
item.classList.add("bg-gray-700");
|
321
|
+
|
322
|
+
if (parseInt(item.dataset.index) === currentIndex) {
|
323
|
+
item.classList.remove("bg-gray-700");
|
324
|
+
item.classList.add("bg-blue-800");
|
325
|
+
|
326
|
+
// Show add button for the active table
|
327
|
+
if (addButton) {
|
328
|
+
addButton.classList.remove('hidden');
|
329
|
+
}
|
330
|
+
}
|
331
|
+
});
|
332
|
+
}
|
333
|
+
|
334
|
+
// Initialize based on URL
|
335
|
+
document.addEventListener("DOMContentLoaded", () => {
|
336
|
+
initializeRecords();
|
337
|
+
renderTable();
|
338
|
+
setActiveTableFromUrl();
|
339
|
+
});
|
panther/panel/urls.py
CHANGED
@@ -1,8 +1,13 @@
|
|
1
|
-
from panther.panel.
|
1
|
+
from panther.panel.views import TableView, CreateView, LoginView, DetailView, HomeView
|
2
2
|
|
3
3
|
urls = {
|
4
|
-
'': models_api,
|
5
|
-
'<index>/': documents_api,
|
6
|
-
'<index>/<document_id>/': single_document_api,
|
7
|
-
'health': healthcheck_api,
|
4
|
+
# '': models_api,
|
5
|
+
# '<index>/': documents_api,
|
6
|
+
# '<index>/<document_id>/': single_document_api,
|
7
|
+
# 'health': healthcheck_api,
|
8
|
+
'': HomeView,
|
9
|
+
'<index>/': TableView,
|
10
|
+
'<index>/create/': CreateView,
|
11
|
+
'login/': LoginView,
|
12
|
+
'<index>/<document_id>/': DetailView,
|
8
13
|
}
|