feat(dashboard): add search functionality and polish translations
- Add search functionality to all dashboard components (servers, renders, AI tasks)- Implement real-time filtering by name, type, status and other relevant fields- Update search input styles to match form-field design- Add clear search button functionality- Polish UI translations for headers, buttons and placeholders- Ensure consistent styling across all dashboard componentsfeat/x_gpu/chat_gpt_new_version
parent
e8c234ce51
commit
ae781652de
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './src/redux/store';
|
||||
|
||||
export const wrapRootElement = ({ element }) => (
|
||||
<Provider store={store}>
|
||||
{element}
|
||||
</Provider>
|
||||
);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './src/redux/store';
|
||||
|
||||
export const wrapRootElement = ({ element }) => (
|
||||
<Provider store={store}>
|
||||
{element}
|
||||
</Provider>
|
||||
);
|
||||
|
|
@ -9,11 +9,13 @@
|
|||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"axios": "^1.7.9",
|
||||
"@reduxjs/toolkit": "^2.6.0",
|
||||
"axios": "^1.8.1",
|
||||
"gatsby": "^5.13.3",
|
||||
"gatsby-plugin-sass": "^6.14.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.1.5",
|
||||
"react-tsparticles": "^2.12.2",
|
||||
"sass": "^1.32.7"
|
||||
|
|
@ -4130,6 +4132,40 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.0.tgz",
|
||||
"integrity": "sha512-mWJCYpewLRyTuuzRSEC/IwIBBkYg2dKtQas8mty5MaV2iXzcmicS3gW554FDeOvLnY3x13NIk8MB1e8wHO7rqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immer": "^10.0.3",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit/node_modules/immer": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||
|
|
@ -4469,6 +4505,12 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz",
|
||||
"integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0="
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yoga-layout": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz",
|
||||
|
|
@ -5415,9 +5457,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
|
||||
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
|
|
@ -9774,6 +9816,15 @@
|
|||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gatsby-cli/node_modules/redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/gatsby-core-utils": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/gatsby-core-utils/-/gatsby-core-utils-4.14.0.tgz",
|
||||
|
|
@ -10293,6 +10344,24 @@
|
|||
"webpack": "^5.59.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gatsby/node_modules/redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/gatsby/node_modules/redux-thunk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
|
||||
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
|
|
@ -14391,6 +14460,29 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25 || ^19",
|
||||
"react": "^18.0 || ^19",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
|
||||
|
|
@ -14602,19 +14694,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
|
||||
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^4"
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
|
|
@ -14850,6 +14941,12 @@
|
|||
"resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz",
|
||||
"integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q=="
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
|
|
@ -16985,6 +17082,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
|
||||
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
|
@ -20330,6 +20436,24 @@
|
|||
"config-chain": "^1.1.11"
|
||||
}
|
||||
},
|
||||
"@reduxjs/toolkit": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.0.tgz",
|
||||
"integrity": "sha512-mWJCYpewLRyTuuzRSEC/IwIBBkYg2dKtQas8mty5MaV2iXzcmicS3gW554FDeOvLnY3x13NIk8MB1e8wHO7rqQ==",
|
||||
"requires": {
|
||||
"immer": "^10.0.3",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"immer": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sideway/address": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||
|
|
@ -20627,6 +20751,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz",
|
||||
"integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0="
|
||||
},
|
||||
"@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
|
||||
},
|
||||
"@types/yoga-layout": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz",
|
||||
|
|
@ -21282,9 +21411,9 @@
|
|||
"integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
|
||||
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
|
|
@ -24551,6 +24680,20 @@
|
|||
"loose-envify": "^1.1.0",
|
||||
"neo-async": "^2.6.1"
|
||||
}
|
||||
},
|
||||
"redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"redux-thunk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
|
||||
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -24599,6 +24742,16 @@
|
|||
"yargs": "^15.4.1",
|
||||
"yoga-layout-prebuilt": "^1.10.0",
|
||||
"yurnalist": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gatsby-core-utils": {
|
||||
|
|
@ -27608,6 +27761,15 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"requires": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"react-refresh": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
|
||||
|
|
@ -27738,17 +27900,14 @@
|
|||
}
|
||||
},
|
||||
"redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
|
||||
},
|
||||
"redux-thunk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
|
||||
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"requires": {}
|
||||
},
|
||||
"reflect.getprototypeof": {
|
||||
|
|
@ -27923,6 +28082,11 @@
|
|||
"resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz",
|
||||
"integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q=="
|
||||
},
|
||||
"reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
|
|
@ -29420,6 +29584,12 @@
|
|||
"schema-utils": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
|
||||
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
|
||||
"requires": {}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"axios": "^1.7.9",
|
||||
"@reduxjs/toolkit": "^2.6.0",
|
||||
"axios": "^1.8.1",
|
||||
"gatsby": "^5.13.3",
|
||||
"gatsby-plugin-sass": "^6.14.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.1.5",
|
||||
"react-tsparticles": "^2.12.2",
|
||||
"sass": "^1.32.7"
|
||||
|
|
|
|||
|
|
@ -2,27 +2,25 @@ import React, { useState } from 'react'
|
|||
import { useSelector, useDispatch } from 'react-redux'
|
||||
|
||||
import { userAuthSelector } from '../../../redux/slices/userAuthSlice'
|
||||
import { modelCrudSelector } from '../../../redux/slices/modelCrudSlice'
|
||||
import modelCrudAsyncThunk from '../../../redux/asyncThunks/modelCrudAsyncThunk'
|
||||
import { threeDModelCrudSelector } from '../../../redux/slices/threeDModelCrudSlice'
|
||||
import { uploadModel } from '../../../redux/asyncThunks/threeDModelCrudAsyncThunk'
|
||||
|
||||
import FormGenerator from '../formGenerator'
|
||||
|
||||
|
||||
const ModelUploadForm = () => {
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [blend, setBlend] = useState('')
|
||||
const [blendInfo, setBlendInfo] = useState('Drop/Click\nfor upload "*.blend" file')
|
||||
|
||||
const { upload_blend_file_status } = useSelector( modelCrudSelector )
|
||||
const { upload_blend_file_status } = useSelector( threeDModelCrudSelector )
|
||||
const { user, token } = useSelector( userAuthSelector )
|
||||
|
||||
let inputList = [
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: 'Upload',
|
||||
endpint: 'model/upload',
|
||||
endpoint: 'model/upload',
|
||||
button_value: 'Upload Model'
|
||||
},
|
||||
{
|
||||
|
|
@ -37,20 +35,19 @@ const ModelUploadForm = () => {
|
|||
]
|
||||
|
||||
const handleModelUpload = () => {
|
||||
let body = {
|
||||
if (!blend) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch( uploadModel({
|
||||
user_id: user.id,
|
||||
file: blend,
|
||||
token: token
|
||||
}
|
||||
console.log( body )
|
||||
dispatch( modelCrudAsyncThunk.fetchUploadModel( body ) )
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="float_form_model"
|
||||
style={ { marginTop: '17%'} }
|
||||
>
|
||||
<div>
|
||||
<FormGenerator
|
||||
inputList={ inputList }
|
||||
refList={ [] }
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useState } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
|
||||
import { userAuthSelector } from '../../../redux/slices/userAuthSlice'
|
||||
import { aiModelCrudSelector } from '../../../redux/slices/aiModelCrudSlice'
|
||||
import { uploadModel } from '../../../redux/asyncThunks/aiModelCrudAsyncThunk'
|
||||
|
||||
import FormGenerator from '../formGenerator'
|
||||
|
||||
const AIModelUploadForm = () => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [model, setModel] = useState('')
|
||||
const [modelInfo, setModelInfo] = useState('Drop/Click\nfor upload AI model file')
|
||||
|
||||
const { upload_model_status } = useSelector(aiModelCrudSelector)
|
||||
const { user, token } = useSelector(userAuthSelector)
|
||||
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: 'Upload',
|
||||
endpoint: 'ai-model/upload',
|
||||
button_value: 'Upload AI Model'
|
||||
},
|
||||
{
|
||||
type: 'file',
|
||||
name: 'Model',
|
||||
fileType: 'ai',
|
||||
dropInfo: modelInfo,
|
||||
setDropInfo: setModelInfo,
|
||||
file: model,
|
||||
setFile: setModel
|
||||
}
|
||||
]
|
||||
|
||||
const handleModelUpload = () => {
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(uploadModel({
|
||||
user_id: user.id,
|
||||
file: model,
|
||||
token: token
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormGenerator
|
||||
inputList={inputList}
|
||||
refList={[]}
|
||||
action={handleModelUpload}
|
||||
/>
|
||||
<p>
|
||||
{
|
||||
!upload_model_status
|
||||
? ''
|
||||
: typeof upload_model_status === 'string'
|
||||
? ''
|
||||
: 'info' in upload_model_status
|
||||
? upload_model_status.info
|
||||
: ''
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIModelUploadForm
|
||||
|
|
@ -392,7 +392,7 @@ const DownloadFilesListInputGenerator = ({
|
|||
* Text input generator, example:
|
||||
* @param {
|
||||
* {
|
||||
* type: 'chice-listing',
|
||||
* type: 'choice-listing',
|
||||
* name: 'name',
|
||||
* values: list,
|
||||
* ref: React.createRef()
|
||||
|
|
@ -488,21 +488,35 @@ const UploadInputGenerator = ({
|
|||
|
||||
const setDropInfos = (name, size) => {
|
||||
input.setDropInfo(
|
||||
'name: "'
|
||||
+ name
|
||||
+ '"\nsize: '
|
||||
+ (Math.round(size / 100 + 'e-2') / 100)
|
||||
+ ' MB'
|
||||
{
|
||||
name: name,
|
||||
size: (Math.round(size / 100 + 'e-2') / 100) + ' MB'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div onDrop={event => onLoadFileDrop(event)} >
|
||||
<pre style={{ marginLeft: '40px' }}>
|
||||
{input.dropInfo}
|
||||
</pre>
|
||||
<div
|
||||
onDrop={event => onLoadFileDrop(event)}
|
||||
className='upload_input_container'
|
||||
>
|
||||
<p>
|
||||
{
|
||||
typeof input.dropInfo === 'string' ?
|
||||
input.dropInfo
|
||||
:
|
||||
input.dropInfo.name
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
{
|
||||
typeof input.dropInfo === 'string' ?
|
||||
""
|
||||
:
|
||||
input.dropInfo.size
|
||||
}
|
||||
</p>
|
||||
<input
|
||||
style={{ marginTop: '-55px' }}
|
||||
id={input.name + info.action + info.endpoint + 'Input'}
|
||||
className='upload_input'
|
||||
type='file'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* Generic List Generator Component
|
||||
|
|
@ -25,6 +25,31 @@ export const ListGenerator = ({
|
|||
const [itemBeingUpdated, setItemBeingUpdated] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
|
||||
const pageOptions = [5, 10, 15, 25, 50];
|
||||
const totalPages = Math.ceil(data.length / itemsPerPage);
|
||||
|
||||
// Oblicz aktualnie wyświetlane elementy
|
||||
const currentItems = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
return data.slice(startIndex, startIndex + itemsPerPage);
|
||||
}, [data, currentPage, itemsPerPage]);
|
||||
|
||||
// Obsługa zmiany strony
|
||||
const handlePageChange = (newPage) => {
|
||||
setCurrentPage(newPage);
|
||||
setSelectedItem(null);
|
||||
};
|
||||
|
||||
// Obsługa zmiany liczby elementów na stronie
|
||||
const handleItemsPerPageChange = (event) => {
|
||||
const newItemsPerPage = parseInt(event.target.value);
|
||||
setItemsPerPage(newItemsPerPage);
|
||||
setCurrentPage(1);
|
||||
setSelectedItem(null);
|
||||
};
|
||||
|
||||
// Toggle the "create" form
|
||||
const handleToggleCreate = () => {
|
||||
|
|
@ -135,7 +160,7 @@ export const ListGenerator = ({
|
|||
No items found. {onCreate && `Click '+ ${title || 'Item'}' to add new items.`}
|
||||
</div>
|
||||
) : (
|
||||
data.map((item) => (
|
||||
currentItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`item-row ${selectedItem?.id === item.id ? 'selected' : ''}`}
|
||||
|
|
@ -203,6 +228,52 @@ export const ListGenerator = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{data.length > 0 && (
|
||||
<div className="pagination-controls">
|
||||
<div className="items-per-page">
|
||||
<span>Items per page:</span>
|
||||
<select value={itemsPerPage} onChange={handleItemsPerPageChange}>
|
||||
{pageOptions.map(option => (
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="pagination-buttons">
|
||||
<button
|
||||
onClick={() => handlePageChange(1)}
|
||||
disabled={currentPage === 1}
|
||||
className="pagination-button"
|
||||
>
|
||||
<<
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="pagination-button"
|
||||
>
|
||||
<
|
||||
</button>
|
||||
<span className="page-info">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="pagination-button"
|
||||
>
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePageChange(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="pagination-button"
|
||||
>
|
||||
>>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="element-details">
|
||||
{selectedItem ? (
|
||||
<div className="details-content">
|
||||
|
|
|
|||
|
|
@ -63,10 +63,10 @@ const DashboardPage = () => {
|
|||
onClick={() => handleNavigation('servers')}
|
||||
>
|
||||
<i className={"fas " + icons_size + " fa-server"}></i>
|
||||
<p>Dashboard</p>
|
||||
<p>GPU Instances</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Rendering</p>
|
||||
<p>3D Stuff</p>
|
||||
<ol>
|
||||
<li
|
||||
className={isActive('3d-models') ? 'active' : ''}
|
||||
|
|
@ -80,10 +80,10 @@ const DashboardPage = () => {
|
|||
onClick={() => handleNavigation('renders')}
|
||||
>
|
||||
<i className={"fas " + icons_size + " fa-paint-brush"}></i>
|
||||
<p>Rendered Materials</p>
|
||||
<p>3D Rendering</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>AI Training</p>
|
||||
<p>AI Stuff</p>
|
||||
<ol>
|
||||
<li
|
||||
className={isActive('ai-models') ? 'active' : ''}
|
||||
|
|
@ -97,7 +97,7 @@ const DashboardPage = () => {
|
|||
onClick={() => handleNavigation('ai-tasks')}
|
||||
>
|
||||
<i className={"fas " + icons_size + " fa-microchip"}></i>
|
||||
<p>AI Training Tasks</p>
|
||||
<p>AI Training</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>User</p>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,244 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { ListGenerator } from '../../components/forms/listGenerator';
|
||||
import { FormGenerator } from '../../components/forms/formGenerator';
|
||||
import ModelUploadForm from '../../components/forms/3d_model_crud/threeDModelUpload';
|
||||
|
||||
const ThreeDModelsDashboard = () => {
|
||||
const [selectedModel, setSelectedModel] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [models, setModels] = useState([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Model A',
|
||||
type: '3D',
|
||||
name: 'Dragon Model',
|
||||
type: 'Blender',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
size: '2.5MB'
|
||||
version: '2.1'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Model B',
|
||||
type: '3D',
|
||||
name: 'Medieval Castle',
|
||||
type: 'Maya',
|
||||
status: 'Inactive',
|
||||
lastModified: '2024-03-19',
|
||||
size: '1.8MB'
|
||||
version: '1.5'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Sci-fi Weapon',
|
||||
type: '3ds Max',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
version: '1.0'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Forest Scene',
|
||||
type: 'Blender',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-18',
|
||||
version: '3.2'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Robot Character',
|
||||
type: 'Maya',
|
||||
status: 'Inactive',
|
||||
lastModified: '2024-03-17',
|
||||
version: '2.0'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Space Ship',
|
||||
type: '3ds Max',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
version: '1.8'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Ancient Temple',
|
||||
type: 'Blender',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-19',
|
||||
version: '2.4'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Fantasy Sword',
|
||||
type: 'Maya',
|
||||
status: 'Inactive',
|
||||
lastModified: '2024-03-16',
|
||||
version: '1.2'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'City Block',
|
||||
type: '3ds Max',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
version: '2.7'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Warrior Character',
|
||||
type: 'Blender',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-18',
|
||||
version: '1.9'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'Futuristic Car',
|
||||
type: 'Maya',
|
||||
status: 'Inactive',
|
||||
lastModified: '2024-03-15',
|
||||
version: '1.3'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Mountain Range',
|
||||
type: '3ds Max',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
version: '2.2'
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'Alien Creature',
|
||||
type: 'Blender',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-19',
|
||||
version: '1.6'
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: 'Magic Staff',
|
||||
type: 'Maya',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
version: '1.4'
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'Underground Cave',
|
||||
type: '3ds Max',
|
||||
status: 'Inactive',
|
||||
lastModified: '2024-03-17',
|
||||
version: '2.3'
|
||||
}
|
||||
]);
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [formMode, setFormMode] = useState('create'); // 'create' lub 'edit'
|
||||
const [formMode, setFormMode] = useState('create');
|
||||
const [message, setMessage] = useState({ type: '', text: '' });
|
||||
|
||||
const mockModels = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Dragon Model",
|
||||
type: "Character",
|
||||
status: "Completed",
|
||||
progress: 100
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Medieval Castle",
|
||||
type: "Environment",
|
||||
status: "In Progress",
|
||||
progress: 65
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Sci-fi Weapon",
|
||||
type: "Prop",
|
||||
status: "Queued",
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Forest Scene",
|
||||
type: "Environment",
|
||||
status: "Completed",
|
||||
progress: 100
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Robot Character",
|
||||
type: "Character",
|
||||
status: "In Progress",
|
||||
progress: 45
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Space Ship",
|
||||
type: "Vehicle",
|
||||
status: "Completed",
|
||||
progress: 100
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Ancient Temple",
|
||||
type: "Environment",
|
||||
status: "In Progress",
|
||||
progress: 78
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Fantasy Sword",
|
||||
type: "Prop",
|
||||
status: "Queued",
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "City Block",
|
||||
type: "Environment",
|
||||
status: "Completed",
|
||||
progress: 100
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Warrior Character",
|
||||
type: "Character",
|
||||
status: "In Progress",
|
||||
progress: 89
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Futuristic Car",
|
||||
type: "Vehicle",
|
||||
status: "Queued",
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Mountain Range",
|
||||
type: "Environment",
|
||||
status: "Completed",
|
||||
progress: 100
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Alien Creature",
|
||||
type: "Character",
|
||||
status: "In Progress",
|
||||
progress: 34
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Magic Staff",
|
||||
type: "Prop",
|
||||
status: "Completed",
|
||||
progress: 100
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Underground Cave",
|
||||
type: "Environment",
|
||||
status: "In Progress",
|
||||
progress: 56
|
||||
}
|
||||
];
|
||||
|
||||
const handleModelSelect = (model) => {
|
||||
setSelectedModel(model);
|
||||
};
|
||||
|
|
@ -60,28 +273,7 @@ const ThreeDModelsDashboard = () => {
|
|||
if (selectedModel?.id === modelId) {
|
||||
setSelectedModel(null);
|
||||
}
|
||||
setMessage({ type: 'success', text: 'Model has been deleted' });
|
||||
};
|
||||
|
||||
const handleFormSubmit = (formData) => {
|
||||
if (formMode === 'create') {
|
||||
const newModel = {
|
||||
id: models.length + 1,
|
||||
...formData,
|
||||
lastModified: new Date().toISOString().split('T')[0],
|
||||
status: 'Active'
|
||||
};
|
||||
setModels([...models, newModel]);
|
||||
setMessage({ type: 'success', text: 'Model has been created' });
|
||||
} else {
|
||||
setModels(models.map(model =>
|
||||
model.id === selectedModel.id
|
||||
? { ...model, ...formData, lastModified: new Date().toISOString().split('T')[0] }
|
||||
: model
|
||||
));
|
||||
setMessage({ type: 'success', text: 'Model has been updated' });
|
||||
}
|
||||
setIsFormVisible(false);
|
||||
setMessage({ type: 'success', text: '3D Model has been deleted' });
|
||||
};
|
||||
|
||||
const handleFormCancel = () => {
|
||||
|
|
@ -104,50 +296,45 @@ const ThreeDModelsDashboard = () => {
|
|||
];
|
||||
};
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Model Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
value: selectedModel?.name || ''
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Model Type',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: '3D', label: '3D' },
|
||||
{ value: '2D', label: '2D' }
|
||||
],
|
||||
value: selectedModel?.type || '3D'
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'Active', label: 'Active' },
|
||||
{ value: 'Inactive', label: 'Inactive' }
|
||||
],
|
||||
value: selectedModel?.status || 'Active'
|
||||
}
|
||||
];
|
||||
const filteredModels = useMemo(() => {
|
||||
return models.filter(model =>
|
||||
model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
model.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
model.status.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [models, searchQuery]);
|
||||
|
||||
return (
|
||||
<div className="list-container">
|
||||
<div className="dashboard-header">
|
||||
<h2>3D Models</h2>
|
||||
<div className="dashboard-controls">
|
||||
<div className="search-container">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Szukaj modeli..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
className="clear-search"
|
||||
onClick={() => setSearchQuery('')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="create-button"
|
||||
onClick={handleCreateModel}
|
||||
>
|
||||
<i className="fas fa-plus"></i>
|
||||
3D Model
|
||||
<i className="fas fa-upload"></i>
|
||||
Upload 3D Model
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message.text && (
|
||||
<div className={`message ${message.type}`}>
|
||||
|
|
@ -158,18 +345,13 @@ const ThreeDModelsDashboard = () => {
|
|||
{isFormVisible && (
|
||||
<div className="form-overlay">
|
||||
<div className="form-container">
|
||||
<FormGenerator
|
||||
fields={formFields}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={handleFormCancel}
|
||||
title={formMode === 'create' ? 'Create new model' : 'Edit model'}
|
||||
/>
|
||||
<ModelUploadForm/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ListGenerator
|
||||
data={models}
|
||||
data={filteredModels}
|
||||
selectedItem={selectedModel}
|
||||
onItemSelect={handleModelSelect}
|
||||
onItemAction={handleModelAction}
|
||||
|
|
@ -184,26 +366,30 @@ const ThreeDModelsDashboard = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="item-details">
|
||||
<div>Version: {model.version}</div>
|
||||
<div>Last Modified: {model.lastModified}</div>
|
||||
<div>Size: {model.size}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
renderDetails={(model) => (
|
||||
<div className="details-panel">
|
||||
<h3>Model Details</h3>
|
||||
<h3>3D Model Details</h3>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">ID:</span>
|
||||
<span className="detail-value">{model.id}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">Nazwa:</span>
|
||||
<span className="detail-label">Name:</span>
|
||||
<span className="detail-value">{model.name}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">Typ:</span>
|
||||
<span className="detail-label">Type:</span>
|
||||
<span className="detail-value">{model.type}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">Version:</span>
|
||||
<span className="detail-value">{model.version}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">Status:</span>
|
||||
<span className="detail-value">{model.status}</span>
|
||||
|
|
@ -212,10 +398,6 @@ const ThreeDModelsDashboard = () => {
|
|||
<span className="detail-label">Last Modified:</span>
|
||||
<span className="detail-value">{model.lastModified}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">Size:</span>
|
||||
<span className="detail-value">{model.size}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useMemo } from 'react';
|
||||
import { ListGenerator } from '../../components/forms/listGenerator';
|
||||
import FormGenerator from '../../components/forms/formGenerator';
|
||||
import AIModelUploadForm from '../../components/forms/ai_model_crud/AIModelUpload';
|
||||
|
||||
const AIModelsDashboard = () => {
|
||||
const [selectedModel, setSelectedModel] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [models, setModels] = useState([
|
||||
{
|
||||
id: 1,
|
||||
|
|
@ -20,12 +22,178 @@ const AIModelsDashboard = () => {
|
|||
status: 'Inactive',
|
||||
lastModified: '2024-03-19',
|
||||
version: '1.0'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'yolo-v8',
|
||||
type: 'object-detection',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
version: '1.2'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'wav2vec',
|
||||
type: 'speech-recognition',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-18',
|
||||
version: '3.0'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'bert-base',
|
||||
type: 'text-classification',
|
||||
status: 'Inactive',
|
||||
lastModified: '2024-03-17',
|
||||
version: '2.0'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'resnet-50',
|
||||
type: 'image-classification',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
version: '1.8'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'detr',
|
||||
type: 'object-detection',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-19',
|
||||
version: '2.4'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'whisper',
|
||||
type: 'speech-recognition',
|
||||
status: 'Inactive',
|
||||
lastModified: '2024-03-16',
|
||||
version: '1.2'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'dalle-3',
|
||||
type: 'text-to-image',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
version: '2.7'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'llama-2',
|
||||
type: 'text-generation',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-18',
|
||||
version: '1.9'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'mask-rcnn',
|
||||
type: 'image-segmentation',
|
||||
status: 'Inactive',
|
||||
lastModified: '2024-03-15',
|
||||
version: '1.3'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'roberta',
|
||||
type: 'text-classification',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
version: '2.2'
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'dino',
|
||||
type: 'image-classification',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-19',
|
||||
version: '1.6'
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: 'sam',
|
||||
type: 'image-segmentation',
|
||||
status: 'Active',
|
||||
lastModified: '2024-03-20',
|
||||
version: '1.4'
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'clip',
|
||||
type: 'image-text',
|
||||
status: 'Inactive',
|
||||
lastModified: '2024-03-17',
|
||||
version: '2.3'
|
||||
}
|
||||
]);
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [formMode, setFormMode] = useState('create');
|
||||
const [message, setMessage] = useState({ type: '', text: '' });
|
||||
|
||||
const nameInput = React.createRef();
|
||||
const typeInput = React.createRef();
|
||||
const versionInput = React.createRef();
|
||||
const statusInput = React.createRef();
|
||||
|
||||
const formRefs = [
|
||||
nameInput,
|
||||
typeInput,
|
||||
versionInput,
|
||||
statusInput
|
||||
];
|
||||
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: formMode === 'create' ? 'Create' : 'Update',
|
||||
endpoint: 'ai/models',
|
||||
button_value: formMode === 'create' ? '+ AI MODEL' : 'UPDATE',
|
||||
allowButtonAction: false
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'NAME',
|
||||
ref: nameInput,
|
||||
value: selectedModel?.name || '',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'TYPE',
|
||||
ref: typeInput,
|
||||
options: [
|
||||
{ value: 'text-to-image', label: 'Text to Image' },
|
||||
{ value: 'image-to-text', label: 'Image to Text' }
|
||||
],
|
||||
value: selectedModel?.type || 'text-to-image',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'VERSION',
|
||||
ref: versionInput,
|
||||
value: selectedModel?.version || '1.0',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'STATUS',
|
||||
ref: statusInput,
|
||||
options: [
|
||||
{ value: 'Active', label: 'Active' },
|
||||
{ value: 'Inactive', label: 'Inactive' }
|
||||
],
|
||||
value: selectedModel?.status || 'Active',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
}
|
||||
];
|
||||
|
||||
const handleModelSelect = (model) => {
|
||||
setSelectedModel(model);
|
||||
};
|
||||
|
|
@ -104,57 +272,45 @@ const AIModelsDashboard = () => {
|
|||
];
|
||||
};
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Model Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
value: selectedModel?.name || ''
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Model Type',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'text-to-image', label: 'Text to Image' },
|
||||
{ value: 'image-to-text', label: 'Image to Text' }
|
||||
],
|
||||
value: selectedModel?.type || 'text-to-image'
|
||||
},
|
||||
{
|
||||
name: 'version',
|
||||
label: 'Version',
|
||||
type: 'text',
|
||||
required: true,
|
||||
value: selectedModel?.version || '1.0'
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'Active', label: 'Active' },
|
||||
{ value: 'Inactive', label: 'Inactive' }
|
||||
],
|
||||
value: selectedModel?.status || 'Active'
|
||||
}
|
||||
];
|
||||
const filteredModels = useMemo(() => {
|
||||
return models.filter(model =>
|
||||
model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
model.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
model.status.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [models, searchQuery]);
|
||||
|
||||
return (
|
||||
<div className="list-container">
|
||||
<div className="dashboard-header">
|
||||
<h2>AI Models</h2>
|
||||
<div className="dashboard-controls">
|
||||
<div className="search-container">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Szukaj modeli..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
className="clear-search"
|
||||
onClick={() => setSearchQuery('')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="create-button"
|
||||
onClick={handleCreateModel}
|
||||
onClick={() => setIsFormVisible(true)}
|
||||
>
|
||||
<i className="fas fa-plus"></i>
|
||||
AI Model
|
||||
<i className="fas fa-upload"></i>
|
||||
Upload AI Model
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message.text && (
|
||||
<div className={`message ${message.type}`}>
|
||||
|
|
@ -165,18 +321,13 @@ const AIModelsDashboard = () => {
|
|||
{isFormVisible && (
|
||||
<div className="form-overlay">
|
||||
<div className="form-container">
|
||||
<FormGenerator
|
||||
fields={formFields}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={handleFormCancel}
|
||||
title={formMode === 'create' ? 'Create new AI Model' : 'Edit AI Model'}
|
||||
/>
|
||||
<AIModelUploadForm />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ListGenerator
|
||||
data={models}
|
||||
data={filteredModels}
|
||||
selectedItem={selectedModel}
|
||||
onItemSelect={handleModelSelect}
|
||||
onItemAction={handleModelAction}
|
||||
|
|
@ -204,15 +355,15 @@ const AIModelsDashboard = () => {
|
|||
<span className="detail-value">{model.id}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">Nazwa:</span>
|
||||
<span className="detail-label">Name:</span>
|
||||
<span className="detail-value">{model.name}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">Typ:</span>
|
||||
<span className="detail-label">Type:</span>
|
||||
<span className="detail-value">{model.type}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">Wersja:</span>
|
||||
<span className="detail-label">Version:</span>
|
||||
<span className="detail-value">{model.version}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
|
|
@ -220,7 +371,7 @@ const AIModelsDashboard = () => {
|
|||
<span className="detail-value">{model.status}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">Ostatnia modyfikacja:</span>
|
||||
<span className="detail-label">Last Modified:</span>
|
||||
<span className="detail-value">{model.lastModified}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,232 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useMemo } from 'react';
|
||||
import { ListGenerator } from '../../components/forms/listGenerator';
|
||||
import { FormGenerator } from '../../components/forms/formGenerator';
|
||||
import FormGenerator from '../../components/forms/formGenerator';
|
||||
|
||||
const AITasksDashboard = () => {
|
||||
const [selectedTask, setSelectedTask] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [tasks, setTasks] = useState([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Task A',
|
||||
type: 'Training',
|
||||
status: 'In Progress',
|
||||
lastModified: '2024-03-20',
|
||||
progress: 45
|
||||
name: "Model Training - CNN",
|
||||
type: "Training",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Object Detection v2",
|
||||
startTime: "2024-03-20 09:00:00",
|
||||
endTime: "2024-03-20 14:30:00"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Task B',
|
||||
type: 'Inference',
|
||||
status: 'Completed',
|
||||
lastModified: '2024-03-19',
|
||||
progress: 100
|
||||
name: "BERT Fine-tuning",
|
||||
type: "Fine-tuning",
|
||||
status: "In Progress",
|
||||
progress: 75,
|
||||
model: "Text Generator",
|
||||
startTime: "2024-03-20 10:15:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Model Evaluation",
|
||||
type: "Evaluation",
|
||||
status: "Queued",
|
||||
progress: 0,
|
||||
model: "Style Transfer v1",
|
||||
startTime: null,
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Performance Testing",
|
||||
type: "Testing",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Face Recognition",
|
||||
startTime: "2024-03-19 15:00:00",
|
||||
endTime: "2024-03-19 17:30:00"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Model Training - RNN",
|
||||
type: "Training",
|
||||
status: "In Progress",
|
||||
progress: 60,
|
||||
model: "Language Translator",
|
||||
startTime: "2024-03-20 08:45:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "GAN Training",
|
||||
type: "Training",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Image Generation",
|
||||
startTime: "2024-03-19 11:00:00",
|
||||
endTime: "2024-03-19 18:30:00"
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Model Optimization",
|
||||
type: "Fine-tuning",
|
||||
status: "In Progress",
|
||||
progress: 82,
|
||||
model: "Voice Synthesis",
|
||||
startTime: "2024-03-20 09:30:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Accuracy Testing",
|
||||
type: "Testing",
|
||||
status: "Queued",
|
||||
progress: 0,
|
||||
model: "Pose Estimation",
|
||||
startTime: null,
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Transfer Learning",
|
||||
type: "Training",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Scene Understanding",
|
||||
startTime: "2024-03-19 13:15:00",
|
||||
endTime: "2024-03-19 16:45:00"
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Model Validation",
|
||||
type: "Evaluation",
|
||||
status: "In Progress",
|
||||
progress: 45,
|
||||
model: "Text Summarizer",
|
||||
startTime: "2024-03-20 11:00:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Hyperparameter Tuning",
|
||||
type: "Fine-tuning",
|
||||
status: "Queued",
|
||||
progress: 0,
|
||||
model: "Speech Recognition",
|
||||
startTime: null,
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Model Training - YOLO",
|
||||
type: "Training",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Object Tracking",
|
||||
startTime: "2024-03-19 09:00:00",
|
||||
endTime: "2024-03-19 15:30:00"
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Performance Optimization",
|
||||
type: "Fine-tuning",
|
||||
status: "In Progress",
|
||||
progress: 68,
|
||||
model: "Image Segmentation",
|
||||
startTime: "2024-03-20 10:00:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Model Deployment Test",
|
||||
type: "Testing",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Sentiment Analysis",
|
||||
startTime: "2024-03-19 14:00:00",
|
||||
endTime: "2024-03-19 16:00:00"
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Model Training - GPT",
|
||||
type: "Training",
|
||||
status: "In Progress",
|
||||
progress: 92,
|
||||
model: "Chatbot Model",
|
||||
startTime: "2024-03-20 07:30:00",
|
||||
endTime: null
|
||||
}
|
||||
]);
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [formMode, setFormMode] = useState('create');
|
||||
const [message, setMessage] = useState({ type: '', text: '' });
|
||||
|
||||
const nameInput = React.createRef();
|
||||
const typeInput = React.createRef();
|
||||
const statusInput = React.createRef();
|
||||
const progressInput = React.createRef();
|
||||
|
||||
const formRefs = [
|
||||
nameInput,
|
||||
typeInput,
|
||||
statusInput,
|
||||
progressInput
|
||||
];
|
||||
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: formMode === 'create' ? 'Create' : 'Update',
|
||||
endpoint: 'ai/tasks',
|
||||
button_value: formMode === 'create' ? '+ AI TASK' : 'UPDATE',
|
||||
allowButtonAction: false
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'NAME',
|
||||
ref: nameInput,
|
||||
value: selectedTask?.name || '',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'TYPE',
|
||||
ref: typeInput,
|
||||
options: [
|
||||
{ value: 'text-to-image', label: 'Text to Image' },
|
||||
{ value: 'image-to-text', label: 'Image to Text' }
|
||||
],
|
||||
value: selectedTask?.type || 'text-to-image',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'STATUS',
|
||||
ref: statusInput,
|
||||
options: [
|
||||
{ value: 'In Progress', label: 'In Progress' },
|
||||
{ value: 'Completed', label: 'Completed' },
|
||||
{ value: 'Failed', label: 'Failed' },
|
||||
{ value: 'Cancelled', label: 'Cancelled' }
|
||||
],
|
||||
value: selectedTask?.status || 'In Progress',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'PROGRESS',
|
||||
ref: progressInput,
|
||||
value: selectedTask?.progress || 0,
|
||||
min: 0,
|
||||
max: 100,
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
}
|
||||
];
|
||||
|
||||
const handleTaskSelect = (task) => {
|
||||
setSelectedTask(task);
|
||||
};
|
||||
|
|
@ -114,6 +315,14 @@ const AITasksDashboard = () => {
|
|||
setSelectedTask(null);
|
||||
};
|
||||
|
||||
const handleFormAction = (refs) => {
|
||||
const formData = {};
|
||||
refs.forEach((ref, index) => {
|
||||
formData[inputList[index].name] = ref.current.value;
|
||||
});
|
||||
handleFormSubmit(formData);
|
||||
};
|
||||
|
||||
const getTaskActions = (task) => {
|
||||
const actions = [];
|
||||
|
||||
|
|
@ -147,53 +356,46 @@ const AITasksDashboard = () => {
|
|||
return actions;
|
||||
};
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Task Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
value: selectedTask?.name || ''
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Task Type',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'Training', label: 'Training' },
|
||||
{ value: 'Inference', label: 'Inference' },
|
||||
{ value: 'Evaluation', label: 'Evaluation' }
|
||||
],
|
||||
value: selectedTask?.type || 'Training'
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'In Progress', label: 'In Progress' },
|
||||
{ value: 'Completed', label: 'Completed' },
|
||||
{ value: 'Failed', label: 'Failed' },
|
||||
{ value: 'Cancelled', label: 'Cancelled' }
|
||||
],
|
||||
value: selectedTask?.status || 'In Progress'
|
||||
}
|
||||
];
|
||||
const filteredTasks = useMemo(() => {
|
||||
return tasks.filter(task =>
|
||||
task.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
task.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
task.status.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
task.model.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [tasks, searchQuery]);
|
||||
|
||||
return (
|
||||
<div className="list-container">
|
||||
<div className="dashboard-header">
|
||||
<h2>AI Tasks</h2>
|
||||
<h2>AI Training</h2>
|
||||
<div className="dashboard-controls">
|
||||
<div className="search-container">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Szukaj zadań..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
className="clear-search"
|
||||
onClick={() => setSearchQuery('')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="create-button"
|
||||
onClick={handleCreateTask}
|
||||
>
|
||||
<i className="fas fa-plus"></i>
|
||||
Task
|
||||
Create Task
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message.text && (
|
||||
<div className={`message ${message.type}`}>
|
||||
|
|
@ -205,17 +407,17 @@ const AITasksDashboard = () => {
|
|||
<div className="form-overlay">
|
||||
<div className="form-container">
|
||||
<FormGenerator
|
||||
fields={formFields}
|
||||
inputList={inputList}
|
||||
formRefs={formRefs}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={handleFormCancel}
|
||||
title={formMode === 'create' ? 'Create new task' : 'Edit task'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ListGenerator
|
||||
data={tasks}
|
||||
data={filteredTasks}
|
||||
selectedItem={selectedTask}
|
||||
onItemSelect={handleTaskSelect}
|
||||
onItemAction={handleTaskAction}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useMemo } from 'react';
|
||||
import { ListGenerator } from '../../components/forms/listGenerator';
|
||||
import FormGenerator from '../../components/forms/formGenerator';
|
||||
|
||||
|
|
@ -7,26 +7,205 @@ const RendersDashboard = () => {
|
|||
const [renders, setRenders] = useState([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Render A',
|
||||
type: '3D',
|
||||
status: 'Completed',
|
||||
lastModified: '2024-03-20',
|
||||
resolution: '1920x1080',
|
||||
progress: 100
|
||||
name: "Character Animation",
|
||||
type: "Animation",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Hero Character",
|
||||
startTime: "2024-03-20 09:00:00",
|
||||
endTime: "2024-03-20 11:30:00"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Render B',
|
||||
type: '2D',
|
||||
status: 'In Progress',
|
||||
lastModified: '2024-03-19',
|
||||
resolution: '3840x2160',
|
||||
progress: 45
|
||||
name: "Environment Lighting",
|
||||
type: "Still",
|
||||
status: "In Progress",
|
||||
progress: 75,
|
||||
model: "Forest Scene",
|
||||
startTime: "2024-03-20 10:15:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Product Showcase",
|
||||
type: "360 View",
|
||||
status: "Queued",
|
||||
progress: 0,
|
||||
model: "Sports Car",
|
||||
startTime: null,
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Battle Scene",
|
||||
type: "Animation",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Warriors",
|
||||
startTime: "2024-03-19 15:00:00",
|
||||
endTime: "2024-03-19 18:30:00"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Architectural Visualization",
|
||||
type: "Still",
|
||||
status: "In Progress",
|
||||
progress: 60,
|
||||
model: "Modern House",
|
||||
startTime: "2024-03-20 08:45:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Character Portrait",
|
||||
type: "Still",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Fantasy Character",
|
||||
startTime: "2024-03-19 11:00:00",
|
||||
endTime: "2024-03-19 12:30:00"
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Vehicle Animation",
|
||||
type: "Animation",
|
||||
status: "In Progress",
|
||||
progress: 82,
|
||||
model: "Racing Car",
|
||||
startTime: "2024-03-20 09:30:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Product Display",
|
||||
type: "360 View",
|
||||
status: "Queued",
|
||||
progress: 0,
|
||||
model: "Smartphone",
|
||||
startTime: null,
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Nature Scene",
|
||||
type: "Still",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Mountain Landscape",
|
||||
startTime: "2024-03-19 13:15:00",
|
||||
endTime: "2024-03-19 15:45:00"
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Character Walk Cycle",
|
||||
type: "Animation",
|
||||
status: "In Progress",
|
||||
progress: 45,
|
||||
model: "Robot Character",
|
||||
startTime: "2024-03-20 11:00:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Jewelry Showcase",
|
||||
type: "360 View",
|
||||
status: "Queued",
|
||||
progress: 0,
|
||||
model: "Diamond Ring",
|
||||
startTime: null,
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "City Flythrough",
|
||||
type: "Animation",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Future City",
|
||||
startTime: "2024-03-19 09:00:00",
|
||||
endTime: "2024-03-19 14:30:00"
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Interior Design",
|
||||
type: "Still",
|
||||
status: "In Progress",
|
||||
progress: 68,
|
||||
model: "Living Room",
|
||||
startTime: "2024-03-20 10:00:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Product Animation",
|
||||
type: "Animation",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Gaming Console",
|
||||
startTime: "2024-03-19 14:00:00",
|
||||
endTime: "2024-03-19 16:00:00"
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Character Showcase",
|
||||
type: "360 View",
|
||||
status: "In Progress",
|
||||
progress: 92,
|
||||
model: "Superhero",
|
||||
startTime: "2024-03-20 07:30:00",
|
||||
endTime: null
|
||||
}
|
||||
]);
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [formMode, setFormMode] = useState('create');
|
||||
const [message, setMessage] = useState({ type: '', text: '' });
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const nameInput = React.createRef();
|
||||
const typeInput = React.createRef();
|
||||
const resolutionInput = React.createRef();
|
||||
const threeDModelInput = React.createRef();
|
||||
|
||||
const formRefs = [
|
||||
nameInput,
|
||||
typeInput,
|
||||
resolutionInput,
|
||||
threeDModelInput
|
||||
];
|
||||
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: formMode === 'create' ? 'Create' : 'Update',
|
||||
endpoint: 'renders',
|
||||
button_value: formMode === 'create' ? '+ RENDER' : 'UPDATE',
|
||||
allowButtonAction: false
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Name',
|
||||
ref: nameInput,
|
||||
value: selectedRender?.name || '',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Resolution',
|
||||
ref: resolutionInput,
|
||||
value: selectedRender?.resolution || '',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'choice-listing',
|
||||
name: '3D Model',
|
||||
ref: threeDModelInput,
|
||||
values: selectedRender?.threeDModel|| '',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
];
|
||||
|
||||
const handleRenderSelect = (render) => {
|
||||
setSelectedRender(render);
|
||||
|
|
@ -107,72 +286,199 @@ const RendersDashboard = () => {
|
|||
];
|
||||
};
|
||||
|
||||
const formFields = [
|
||||
const mockRenders = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Nazwa renderu',
|
||||
type: 'text',
|
||||
required: true,
|
||||
value: selectedRender?.name || ''
|
||||
id: 1,
|
||||
name: "Dragon Scene",
|
||||
type: "Animation",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Dragon Model",
|
||||
startTime: "2024-03-01 10:00:00",
|
||||
endTime: "2024-03-01 12:30:00"
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Typ renderu',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: '3D', label: '3D' },
|
||||
{ value: '2D', label: '2D' }
|
||||
],
|
||||
value: selectedRender?.type || '3D'
|
||||
id: 2,
|
||||
name: "Castle Exterior",
|
||||
type: "Still",
|
||||
status: "In Progress",
|
||||
progress: 65,
|
||||
model: "Medieval Castle",
|
||||
startTime: "2024-03-02 09:00:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
name: 'resolution',
|
||||
label: 'Rozdzielczość',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: '1920x1080', label: 'Full HD (1920x1080)' },
|
||||
{ value: '3840x2160', label: '4K (3840x2160)' },
|
||||
{ value: '7680x4320', label: '8K (7680x4320)' }
|
||||
],
|
||||
value: selectedRender?.resolution || '1920x1080'
|
||||
id: 3,
|
||||
name: "Weapon Showcase",
|
||||
type: "360 View",
|
||||
status: "Queued",
|
||||
progress: 0,
|
||||
model: "Sci-fi Weapon",
|
||||
startTime: null,
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'In Progress', label: 'W trakcie' },
|
||||
{ value: 'Completed', label: 'Zakończony' },
|
||||
{ value: 'Failed', label: 'Nieudany' }
|
||||
],
|
||||
value: selectedRender?.status || 'In Progress'
|
||||
id: 4,
|
||||
name: "Forest Flythrough",
|
||||
type: "Animation",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Forest Scene",
|
||||
startTime: "2024-03-01 14:00:00",
|
||||
endTime: "2024-03-01 16:00:00"
|
||||
},
|
||||
{
|
||||
name: 'progress',
|
||||
label: 'Postęp',
|
||||
type: 'number',
|
||||
required: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: selectedRender?.progress || 0
|
||||
id: 5,
|
||||
name: "Robot Animation",
|
||||
type: "Animation",
|
||||
status: "In Progress",
|
||||
progress: 45,
|
||||
model: "Robot Character",
|
||||
startTime: "2024-03-02 11:00:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Spaceship Launch",
|
||||
type: "Animation",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Space Ship",
|
||||
startTime: "2024-03-01 13:00:00",
|
||||
endTime: "2024-03-01 15:30:00"
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Temple Interior",
|
||||
type: "Still",
|
||||
status: "In Progress",
|
||||
progress: 78,
|
||||
model: "Ancient Temple",
|
||||
startTime: "2024-03-02 10:00:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Sword Display",
|
||||
type: "360 View",
|
||||
status: "Queued",
|
||||
progress: 0,
|
||||
model: "Fantasy Sword",
|
||||
startTime: null,
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "City Timelapse",
|
||||
type: "Animation",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "City Block",
|
||||
startTime: "2024-03-01 09:00:00",
|
||||
endTime: "2024-03-01 11:30:00"
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Warrior Battle",
|
||||
type: "Animation",
|
||||
status: "In Progress",
|
||||
progress: 89,
|
||||
model: "Warrior Character",
|
||||
startTime: "2024-03-02 13:00:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Car Showcase",
|
||||
type: "360 View",
|
||||
status: "Queued",
|
||||
progress: 0,
|
||||
model: "Futuristic Car",
|
||||
startTime: null,
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Mountain Vista",
|
||||
type: "Still",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Mountain Range",
|
||||
startTime: "2024-03-01 15:00:00",
|
||||
endTime: "2024-03-01 16:30:00"
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Alien Movement",
|
||||
type: "Animation",
|
||||
status: "In Progress",
|
||||
progress: 34,
|
||||
model: "Alien Creature",
|
||||
startTime: "2024-03-02 14:00:00",
|
||||
endTime: null
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Staff Effects",
|
||||
type: "Animation",
|
||||
status: "Completed",
|
||||
progress: 100,
|
||||
model: "Magic Staff",
|
||||
startTime: "2024-03-01 16:00:00",
|
||||
endTime: "2024-03-01 18:30:00"
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Cave Exploration",
|
||||
type: "Animation",
|
||||
status: "In Progress",
|
||||
progress: 56,
|
||||
model: "Underground Cave",
|
||||
startTime: "2024-03-02 12:00:00",
|
||||
endTime: null
|
||||
}
|
||||
];
|
||||
|
||||
const filteredRenders = useMemo(() => {
|
||||
return renders.filter(render =>
|
||||
render.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
render.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
render.status.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
render.model.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [renders, searchQuery]);
|
||||
|
||||
return (
|
||||
<div className="list-container">
|
||||
<div className="dashboard-header">
|
||||
<h2>Rendered Materials</h2>
|
||||
<h2>3D Rendering</h2>
|
||||
<div className="dashboard-controls">
|
||||
<div className="search-container">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Szukaj renderów..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
className="clear-search"
|
||||
onClick={() => setSearchQuery('')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="create-button"
|
||||
onClick={handleCreateRender}
|
||||
>
|
||||
<i className="fas fa-plus"></i>
|
||||
Render
|
||||
Create Render
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message.text && (
|
||||
<div className={`message ${message.type}`}>
|
||||
|
|
@ -184,17 +490,16 @@ const RendersDashboard = () => {
|
|||
<div className="form-overlay">
|
||||
<div className="form-container">
|
||||
<FormGenerator
|
||||
fields={formFields}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={handleFormCancel}
|
||||
title={formMode === 'create' ? 'Utwórz nowy render' : 'Edytuj render'}
|
||||
inputList={inputList}
|
||||
refList={formRefs}
|
||||
action={handleFormSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ListGenerator
|
||||
data={renders}
|
||||
data={filteredRenders}
|
||||
selectedItem={selectedRender}
|
||||
onItemSelect={handleRenderSelect}
|
||||
onItemAction={handleRenderAction}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,214 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useMemo } from 'react';
|
||||
import { ListGenerator } from '../../components/forms/listGenerator';
|
||||
import FormGenerator from '../../components/forms/formGenerator';
|
||||
|
||||
const ServersDashboard = () => {
|
||||
const [selectedServer, setSelectedServer] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [servers, setServers] = useState([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Server A',
|
||||
type: 'Render',
|
||||
status: 'Online',
|
||||
lastModified: '2024-03-20',
|
||||
ip: '192.168.1.100'
|
||||
name: "Render Node 1",
|
||||
type: "Render Node",
|
||||
status: "Active",
|
||||
progress: 100,
|
||||
ip: "192.168.1.101",
|
||||
lastActive: "2024-03-20 11:30:00"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Server B',
|
||||
type: 'AI',
|
||||
status: 'Offline',
|
||||
lastModified: '2024-03-19',
|
||||
ip: '192.168.1.101'
|
||||
name: "AI Training Node 1",
|
||||
type: "AI Training Node",
|
||||
status: "In Progress",
|
||||
progress: 75,
|
||||
ip: "192.168.1.102",
|
||||
lastActive: "2024-03-20 11:29:00"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Storage Server 1",
|
||||
type: "Storage Server",
|
||||
status: "Active",
|
||||
progress: 100,
|
||||
ip: "192.168.1.103",
|
||||
lastActive: "2024-03-20 11:30:00"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Render Node 2",
|
||||
type: "Render Node",
|
||||
status: "Inactive",
|
||||
progress: 0,
|
||||
ip: "192.168.1.104",
|
||||
lastActive: "2024-03-20 10:15:00"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "AI Training Node 2",
|
||||
type: "AI Training Node",
|
||||
status: "Active",
|
||||
progress: 100,
|
||||
ip: "192.168.1.105",
|
||||
lastActive: "2024-03-20 11:30:00"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Storage Server 2",
|
||||
type: "Storage Server",
|
||||
status: "Active",
|
||||
progress: 100,
|
||||
ip: "192.168.1.106",
|
||||
lastActive: "2024-03-20 11:30:00"
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Render Node 3",
|
||||
type: "Render Node",
|
||||
status: "In Progress",
|
||||
progress: 82,
|
||||
ip: "192.168.1.107",
|
||||
lastActive: "2024-03-20 11:29:00"
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "AI Training Node 3",
|
||||
type: "AI Training Node",
|
||||
status: "Queued",
|
||||
progress: 0,
|
||||
ip: "192.168.1.108",
|
||||
lastActive: "2024-03-20 11:00:00"
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Storage Server 3",
|
||||
type: "Storage Server",
|
||||
status: "Active",
|
||||
progress: 100,
|
||||
ip: "192.168.1.109",
|
||||
lastActive: "2024-03-20 11:30:00"
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Render Node 4",
|
||||
type: "Render Node",
|
||||
status: "In Progress",
|
||||
progress: 45,
|
||||
ip: "192.168.1.110",
|
||||
lastActive: "2024-03-20 11:29:00"
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "AI Training Node 4",
|
||||
type: "AI Training Node",
|
||||
status: "Active",
|
||||
progress: 100,
|
||||
ip: "192.168.1.111",
|
||||
lastActive: "2024-03-20 11:30:00"
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Storage Server 4",
|
||||
type: "Storage Server",
|
||||
status: "Inactive",
|
||||
progress: 0,
|
||||
ip: "192.168.1.112",
|
||||
lastActive: "2024-03-20 10:45:00"
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Render Node 5",
|
||||
type: "Render Node",
|
||||
status: "Active",
|
||||
progress: 100,
|
||||
ip: "192.168.1.113",
|
||||
lastActive: "2024-03-20 11:30:00"
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "AI Training Node 5",
|
||||
type: "AI Training Node",
|
||||
status: "In Progress",
|
||||
progress: 68,
|
||||
ip: "192.168.1.114",
|
||||
lastActive: "2024-03-20 11:29:00"
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Storage Server 5",
|
||||
type: "Storage Server",
|
||||
status: "Active",
|
||||
progress: 100,
|
||||
ip: "192.168.1.115",
|
||||
lastActive: "2024-03-20 11:30:00"
|
||||
}
|
||||
]);
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [formMode, setFormMode] = useState('create');
|
||||
const [message, setMessage] = useState({ type: '', text: '' });
|
||||
|
||||
const nameInput = React.createRef();
|
||||
const typeInput = React.createRef();
|
||||
const ipInput = React.createRef();
|
||||
const statusInput = React.createRef();
|
||||
|
||||
const formRefs = [
|
||||
nameInput,
|
||||
typeInput,
|
||||
ipInput,
|
||||
statusInput
|
||||
];
|
||||
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: formMode === 'create' ? 'Create' : 'Update',
|
||||
endpoint: 'servers',
|
||||
button_value: formMode === 'create' ? '+ SERVER' : 'UPDATE'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'NAME',
|
||||
ref: nameInput,
|
||||
value: selectedServer?.name || '',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'TYPE',
|
||||
ref: typeInput,
|
||||
options: [
|
||||
{ value: 'Render', label: 'Render' },
|
||||
{ value: 'AI', label: 'AI' },
|
||||
{ value: 'Storage', label: 'Storage' }
|
||||
],
|
||||
value: selectedServer?.type || 'Render',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'IP',
|
||||
ref: ipInput,
|
||||
value: selectedServer?.ip || '',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'STATUS',
|
||||
ref: statusInput,
|
||||
options: [
|
||||
{ value: 'Online', label: 'Online' },
|
||||
{ value: 'Offline', label: 'Offline' },
|
||||
{ value: 'Maintenance', label: 'Maintenance' }
|
||||
],
|
||||
value: selectedServer?.status || 'Offline',
|
||||
onChange: null,
|
||||
validationInfo: null
|
||||
}
|
||||
];
|
||||
|
||||
const handleServerSelect = (server) => {
|
||||
setSelectedServer(server);
|
||||
};
|
||||
|
|
@ -104,59 +287,54 @@ const ServersDashboard = () => {
|
|||
];
|
||||
};
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Nazwa serwera',
|
||||
type: 'text',
|
||||
required: true,
|
||||
value: selectedServer?.name || ''
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Typ serwera',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'Render', label: 'Render' },
|
||||
{ value: 'AI', label: 'AI' },
|
||||
{ value: 'Storage', label: 'Storage' }
|
||||
],
|
||||
value: selectedServer?.type || 'Render'
|
||||
},
|
||||
{
|
||||
name: 'ip',
|
||||
label: 'Adres IP',
|
||||
type: 'text',
|
||||
required: true,
|
||||
value: selectedServer?.ip || ''
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'Online', label: 'Online' },
|
||||
{ value: 'Offline', label: 'Offline' },
|
||||
{ value: 'Maintenance', label: 'Maintenance' }
|
||||
],
|
||||
value: selectedServer?.status || 'Offline'
|
||||
}
|
||||
];
|
||||
const handleFormAction = (refs) => {
|
||||
const formData = {};
|
||||
refs.forEach((ref, index) => {
|
||||
formData[inputList[index].name] = ref.current.value;
|
||||
});
|
||||
handleFormSubmit(formData);
|
||||
};
|
||||
|
||||
const filteredServers = useMemo(() => {
|
||||
return servers.filter(server =>
|
||||
server.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
server.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
server.status.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
server.ip.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [servers, searchQuery]);
|
||||
|
||||
return (
|
||||
<div className="list-container">
|
||||
<div className="dashboard-header">
|
||||
<h2>Servers</h2>
|
||||
<h2>GPU Instances</h2>
|
||||
<div className="dashboard-controls">
|
||||
<div className="search-container">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Szukaj serwerów..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
className="clear-search"
|
||||
onClick={() => setSearchQuery('')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="create-button"
|
||||
onClick={handleCreateServer}
|
||||
>
|
||||
<i className="fas fa-plus"></i>
|
||||
Server
|
||||
Add Server
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message.text && (
|
||||
<div className={`message ${message.type}`}>
|
||||
|
|
@ -168,17 +346,16 @@ const ServersDashboard = () => {
|
|||
<div className="form-overlay">
|
||||
<div className="form-container">
|
||||
<FormGenerator
|
||||
fields={formFields}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={handleFormCancel}
|
||||
title={formMode === 'create' ? 'Utwórz nowy serwer' : 'Edytuj serwer'}
|
||||
inputList={inputList}
|
||||
refList={formRefs}
|
||||
action={handleFormAction}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ListGenerator
|
||||
data={servers}
|
||||
data={filteredServers}
|
||||
selectedItem={selectedServer}
|
||||
onItemSelect={handleServerSelect}
|
||||
onItemAction={handleServerAction}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,16 @@ const UserSettings = () => {
|
|||
return baseInputs;
|
||||
};
|
||||
|
||||
const formRefs = [usernameInput, emailInput, currentPasswordInput, newPasswordInput, confirmPasswordInput];
|
||||
|
||||
const handleFormAction = (refs) => {
|
||||
const formData = {};
|
||||
formRefs.forEach((ref, index) => {
|
||||
formData[getInputList()[index].name] = ref.current.value;
|
||||
});
|
||||
handleSubmit(formRefs);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="list-container">
|
||||
<div className="user-settings">
|
||||
|
|
@ -157,19 +167,26 @@ const UserSettings = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{isEditing && (
|
||||
<div className="form-overlay">
|
||||
<div className="form-container">
|
||||
<FormGenerator
|
||||
inputList={getInputList()}
|
||||
refList={[
|
||||
usernameInput,
|
||||
emailInput,
|
||||
currentPasswordInput,
|
||||
newPasswordInput,
|
||||
confirmPasswordInput
|
||||
]}
|
||||
action={handleSubmit}
|
||||
inputList={getInputList().map((field, index) => ({
|
||||
...field,
|
||||
ref: formRefs[index],
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
onChange: field.onChange,
|
||||
validationInfo: field.validationInfo
|
||||
}))}
|
||||
refList={formRefs}
|
||||
action={handleFormAction}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
const IndexPage = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Reszta komponentów aplikacji */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexPage;
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
||||
|
||||
// Helper function to get auth header
|
||||
const getAuthHeader = (token) => ({
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
export const fetchAiModels = createAsyncThunk(
|
||||
'aiModelCrud/fetchModels',
|
||||
async (_, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.get(`${API_URL}/ai/models`, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchAiModel = createAsyncThunk(
|
||||
'aiModelCrud/fetchModel',
|
||||
async (modelId, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.get(`${API_URL}/ai/models/${modelId}`, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const createAiModel = createAsyncThunk(
|
||||
'aiModelCrud/createModel',
|
||||
async (modelData, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.post(`${API_URL}/ai/models`, modelData, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const updateAiModel = createAsyncThunk(
|
||||
'aiModelCrud/updateModel',
|
||||
async ({ modelId, updates }, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.put(`${API_URL}/ai/models/${modelId}`, updates, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteAiModel = createAsyncThunk(
|
||||
'aiModelCrud/deleteModel',
|
||||
async (modelId, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
await axios.delete(`${API_URL}/ai/models/${modelId}`, getAuthHeader(token));
|
||||
return modelId;
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchAiTasks = createAsyncThunk(
|
||||
'aiModelCrud/fetchTasks',
|
||||
async ({ status, page = 1, limit = 10 }, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const params = { page, limit };
|
||||
if (status) params.status = status;
|
||||
|
||||
const response = await axios.get(`${API_URL}/ai/tasks`, {
|
||||
...getAuthHeader(token),
|
||||
params
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const createAiTask = createAsyncThunk(
|
||||
'aiModelCrud/createTask',
|
||||
async (taskData, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.post(`${API_URL}/ai/tasks`, taskData, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const updateAiTask = createAsyncThunk(
|
||||
'aiModelCrud/updateTask',
|
||||
async ({ taskId, action }, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.put(
|
||||
`${API_URL}/ai/tasks/${taskId}`,
|
||||
{ action },
|
||||
getAuthHeader(token)
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteAiTask = createAsyncThunk(
|
||||
'aiModelCrud/deleteTask',
|
||||
async (taskId, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
await axios.delete(`${API_URL}/ai/tasks/${taskId}`, getAuthHeader(token));
|
||||
return taskId;
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
||||
|
||||
// Helper function to get auth header
|
||||
const getAuthHeader = (token) => ({
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
export const fetchRenders = createAsyncThunk(
|
||||
'renderCrud/fetchRenders',
|
||||
async ({ page = 1, limit = 10 }, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.get(`${API_URL}/renders`, {
|
||||
...getAuthHeader(token),
|
||||
params: { page, limit }
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchRender = createAsyncThunk(
|
||||
'renderCrud/fetchRender',
|
||||
async (renderId, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.get(`${API_URL}/renders/${renderId}`, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const updateRender = createAsyncThunk(
|
||||
'renderCrud/updateRender',
|
||||
async ({ renderId, updates }, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.put(`${API_URL}/renders/${renderId}`, updates, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteRender = createAsyncThunk(
|
||||
'renderCrud/deleteRender',
|
||||
async (renderId, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
await axios.delete(`${API_URL}/renders/${renderId}`, getAuthHeader(token));
|
||||
return renderId;
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchRenderTasks = createAsyncThunk(
|
||||
'renderCrud/fetchTasks',
|
||||
async ({ status, page = 1, limit = 10 }, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const params = { page, limit };
|
||||
if (status) params.status = status;
|
||||
|
||||
const response = await axios.get(`${API_URL}/renders/tasks`, {
|
||||
...getAuthHeader(token),
|
||||
params
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const createRenderTask = createAsyncThunk(
|
||||
'renderCrud/createTask',
|
||||
async (taskData, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.post(`${API_URL}/renders/tasks`, taskData, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const updateRenderTask = createAsyncThunk(
|
||||
'renderCrud/updateTask',
|
||||
async ({ taskId, action }, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.put(
|
||||
`${API_URL}/renders/tasks/${taskId}`,
|
||||
{ action },
|
||||
getAuthHeader(token)
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteRenderTask = createAsyncThunk(
|
||||
'renderCrud/deleteTask',
|
||||
async (taskId, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
await axios.delete(`${API_URL}/renders/tasks/${taskId}`, getAuthHeader(token));
|
||||
return taskId;
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
||||
|
||||
// Helper function to get auth header
|
||||
const getAuthHeader = (token) => ({
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
export const fetchModels = createAsyncThunk(
|
||||
'threeDModelCrud/fetchModels',
|
||||
async (_, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.get(`${API_URL}/models`, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchModel = createAsyncThunk(
|
||||
'threeDModelCrud/fetchModel',
|
||||
async (modelId, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.get(`${API_URL}/models/${modelId}`, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const createModel = createAsyncThunk(
|
||||
'threeDModelCrud/createModel',
|
||||
async (modelData, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.post(`${API_URL}/models`, modelData, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const updateModel = createAsyncThunk(
|
||||
'threeDModelCrud/updateModel',
|
||||
async ({ modelId, updates }, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
const response = await axios.put(`${API_URL}/models/${modelId}`, updates, getAuthHeader(token));
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteModel = createAsyncThunk(
|
||||
'threeDModelCrud/deleteModel',
|
||||
async (modelId, { getState }) => {
|
||||
const { token } = getState().userAuth;
|
||||
await axios.delete(`${API_URL}/models/${modelId}`, getAuthHeader(token));
|
||||
return modelId;
|
||||
}
|
||||
);
|
||||
|
||||
export const uploadModel = createAsyncThunk(
|
||||
'threeDModelCrud/uploadModel',
|
||||
async ({ user_id, file, token }) => {
|
||||
const formData = new FormData();
|
||||
formData.append('user_id', user_id);
|
||||
formData.append('file', file);
|
||||
formData.append('token', token);
|
||||
|
||||
const response = await axios.post(`${API_URL}/models/upload`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
||||
|
||||
export const loginUser = createAsyncThunk(
|
||||
'userAuth/login',
|
||||
async (credentials) => {
|
||||
const formData = new FormData();
|
||||
formData.append('username', credentials.username);
|
||||
formData.append('password', credentials.password);
|
||||
|
||||
const response = await axios.post(`${API_URL}/auth`, formData);
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const registerUser = createAsyncThunk(
|
||||
'userAuth/register',
|
||||
async (userData) => {
|
||||
const response = await axios.post(`${API_URL}/register`, userData);
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const changePassword = createAsyncThunk(
|
||||
'userAuth/changePassword',
|
||||
async (passwordData) => {
|
||||
const response = await axios.post(`${API_URL}/change-password`, passwordData);
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
fetchAiModels,
|
||||
fetchAiModel,
|
||||
createAiModel,
|
||||
updateAiModel,
|
||||
deleteAiModel,
|
||||
createAiTask,
|
||||
updateAiTask,
|
||||
deleteAiTask,
|
||||
fetchAiTasks
|
||||
} from '../asyncThunks/aiModelCrudAsyncThunk';
|
||||
|
||||
const initialState = {
|
||||
models: [],
|
||||
selectedModel: null,
|
||||
tasks: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
const aiModelCrudSlice = createSlice({
|
||||
name: 'aiModelCrud',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
},
|
||||
setSelectedModel: (state, action) => {
|
||||
state.selectedModel = action.payload;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Fetch Models
|
||||
builder.addCase(fetchAiModels.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchAiModels.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.models = action.payload;
|
||||
});
|
||||
builder.addCase(fetchAiModels.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Fetch Single Model
|
||||
builder.addCase(fetchAiModel.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchAiModel.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.selectedModel = action.payload;
|
||||
});
|
||||
builder.addCase(fetchAiModel.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Create Model
|
||||
builder.addCase(createAiModel.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(createAiModel.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.models.push(action.payload);
|
||||
});
|
||||
builder.addCase(createAiModel.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Update Model
|
||||
builder.addCase(updateAiModel.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(updateAiModel.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
const index = state.models.findIndex(model => model.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.models[index] = action.payload;
|
||||
}
|
||||
});
|
||||
builder.addCase(updateAiModel.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Delete Model
|
||||
builder.addCase(deleteAiModel.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(deleteAiModel.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.models = state.models.filter(model => model.id !== action.payload);
|
||||
});
|
||||
builder.addCase(deleteAiModel.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Fetch Tasks
|
||||
builder.addCase(fetchAiTasks.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchAiTasks.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.tasks = action.payload;
|
||||
});
|
||||
builder.addCase(fetchAiTasks.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Create Task
|
||||
builder.addCase(createAiTask.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(createAiTask.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.tasks.push(action.payload);
|
||||
});
|
||||
builder.addCase(createAiTask.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Update Task
|
||||
builder.addCase(updateAiTask.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(updateAiTask.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
const index = state.tasks.findIndex(task => task.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.tasks[index] = action.payload;
|
||||
}
|
||||
});
|
||||
builder.addCase(updateAiTask.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Delete Task
|
||||
builder.addCase(deleteAiTask.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(deleteAiTask.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.tasks = state.tasks.filter(task => task.id !== action.payload);
|
||||
});
|
||||
builder.addCase(deleteAiTask.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const { clearError, setSelectedModel } = aiModelCrudSlice.actions;
|
||||
export const aiModelCrudSelector = (state) => state.aiModelCrud;
|
||||
export default aiModelCrudSlice.reducer;
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
fetchRenders,
|
||||
fetchRender,
|
||||
updateRender,
|
||||
deleteRender,
|
||||
fetchRenderTasks,
|
||||
createRenderTask,
|
||||
updateRenderTask,
|
||||
deleteRenderTask
|
||||
} from '../asyncThunks/renderCrudAsyncThunk';
|
||||
|
||||
const initialState = {
|
||||
renders: [],
|
||||
selectedRender: null,
|
||||
tasks: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
const renderCrudSlice = createSlice({
|
||||
name: 'renderCrud',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
},
|
||||
setSelectedRender: (state, action) => {
|
||||
state.selectedRender = action.payload;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Fetch Renders
|
||||
builder.addCase(fetchRenders.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchRenders.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.renders = action.payload;
|
||||
});
|
||||
builder.addCase(fetchRenders.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Fetch Single Render
|
||||
builder.addCase(fetchRender.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchRender.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.selectedRender = action.payload;
|
||||
});
|
||||
builder.addCase(fetchRender.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Update Render
|
||||
builder.addCase(updateRender.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(updateRender.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
const index = state.renders.findIndex(render => render.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.renders[index] = action.payload;
|
||||
}
|
||||
});
|
||||
builder.addCase(updateRender.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Delete Render
|
||||
builder.addCase(deleteRender.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(deleteRender.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.renders = state.renders.filter(render => render.id !== action.payload);
|
||||
});
|
||||
builder.addCase(deleteRender.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Fetch Tasks
|
||||
builder.addCase(fetchRenderTasks.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchRenderTasks.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.tasks = action.payload;
|
||||
});
|
||||
builder.addCase(fetchRenderTasks.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Create Task
|
||||
builder.addCase(createRenderTask.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(createRenderTask.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.tasks.push(action.payload);
|
||||
});
|
||||
builder.addCase(createRenderTask.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Update Task
|
||||
builder.addCase(updateRenderTask.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(updateRenderTask.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
const index = state.tasks.findIndex(task => task.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.tasks[index] = action.payload;
|
||||
}
|
||||
});
|
||||
builder.addCase(updateRenderTask.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Delete Task
|
||||
builder.addCase(deleteRenderTask.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(deleteRenderTask.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.tasks = state.tasks.filter(task => task.id !== action.payload);
|
||||
});
|
||||
builder.addCase(deleteRenderTask.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const { clearError, setSelectedRender } = renderCrudSlice.actions;
|
||||
export const renderCrudSelector = (state) => state.renderCrud;
|
||||
export default renderCrudSlice.reducer;
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
fetchModels,
|
||||
fetchModel,
|
||||
createModel,
|
||||
updateModel,
|
||||
deleteModel,
|
||||
uploadModel
|
||||
} from '../asyncThunks/threeDModelCrudAsyncThunk';
|
||||
|
||||
const initialState = {
|
||||
models: [],
|
||||
selectedModel: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
upload_blend_file_status: ''
|
||||
};
|
||||
|
||||
const threeDModelCrudSlice = createSlice({
|
||||
name: 'threeDModelCrud',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
},
|
||||
setSelectedModel: (state, action) => {
|
||||
state.selectedModel = action.payload;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Fetch Models
|
||||
builder.addCase(fetchModels.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchModels.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.models = action.payload;
|
||||
});
|
||||
builder.addCase(fetchModels.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Fetch Single Model
|
||||
builder.addCase(fetchModel.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchModel.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.selectedModel = action.payload;
|
||||
});
|
||||
builder.addCase(fetchModel.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Create Model
|
||||
builder.addCase(createModel.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(createModel.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.models.push(action.payload);
|
||||
});
|
||||
builder.addCase(createModel.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Update Model
|
||||
builder.addCase(updateModel.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(updateModel.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
const index = state.models.findIndex(model => model.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.models[index] = action.payload;
|
||||
}
|
||||
});
|
||||
builder.addCase(updateModel.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Delete Model
|
||||
builder.addCase(deleteModel.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(deleteModel.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.models = state.models.filter(model => model.id !== action.payload);
|
||||
});
|
||||
builder.addCase(deleteModel.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Upload Model
|
||||
builder.addCase(uploadModel.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
state.upload_blend_file_status = 'uploading';
|
||||
});
|
||||
builder.addCase(uploadModel.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.upload_blend_file_status = { info: 'Upload successful' };
|
||||
});
|
||||
builder.addCase(uploadModel.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
state.upload_blend_file_status = { info: 'Upload failed' };
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const { clearError, setSelectedModel } = threeDModelCrudSlice.actions;
|
||||
export const threeDModelCrudSelector = (state) => state.threeDModelCrud;
|
||||
export default threeDModelCrudSlice.reducer;
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { loginUser, registerUser, changePassword } from '../asyncThunks/userAuthAsyncThunk';
|
||||
|
||||
const initialState = {
|
||||
user: null,
|
||||
token: null,
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
const userAuthSlice = createSlice({
|
||||
name: 'userAuth',
|
||||
initialState,
|
||||
reducers: {
|
||||
logout: (state) => {
|
||||
state.user = null;
|
||||
state.token = null;
|
||||
state.error = null;
|
||||
},
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Login
|
||||
builder.addCase(loginUser.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(loginUser.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.user = action.payload.user;
|
||||
state.token = action.payload.token;
|
||||
});
|
||||
builder.addCase(loginUser.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Register
|
||||
builder.addCase(registerUser.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(registerUser.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.user = action.payload.user;
|
||||
state.token = action.payload.token;
|
||||
});
|
||||
builder.addCase(registerUser.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Change Password
|
||||
builder.addCase(changePassword.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(changePassword.fulfilled, (state) => {
|
||||
state.isLoading = false;
|
||||
});
|
||||
builder.addCase(changePassword.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const { logout, clearError } = userAuthSlice.actions;
|
||||
export const userAuthSelector = (state) => state.userAuth;
|
||||
export default userAuthSlice.reducer;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import userAuthReducer from './slices/userAuthSlice';
|
||||
import threeDModelCrudReducer from './slices/threeDModelCrudSlice';
|
||||
import aiModelCrudReducer from './slices/aiModelCrudSlice';
|
||||
import renderCrudReducer from './slices/renderCrudSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
userAuth: userAuthReducer,
|
||||
threeDModelCrud: threeDModelCrudReducer,
|
||||
aiModelCrud: aiModelCrudReducer,
|
||||
renderCrud: renderCrudReducer
|
||||
}
|
||||
});
|
||||
|
|
@ -504,6 +504,75 @@ body, html {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid rgba($subtitle-color, 0.1);
|
||||
|
||||
.items-per-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
span {
|
||||
color: $subtitle-color;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 5px 10px;
|
||||
background: $background-color;
|
||||
color: $title-color;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $first-color;
|
||||
}
|
||||
|
||||
option {
|
||||
background: $background-color;
|
||||
color: $title-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.pagination-button {
|
||||
padding: 5px 10px;
|
||||
background: $background-color;
|
||||
color: $title-color;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: $first-color;
|
||||
border-color: $first-color;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.page-info {
|
||||
color: $subtitle-color;
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-row {
|
||||
|
|
@ -1046,25 +1115,29 @@ body, html {
|
|||
}
|
||||
}
|
||||
|
||||
.float_form_model {
|
||||
position: fixed;
|
||||
width: 400px;
|
||||
padding: 50px;
|
||||
.upload_input_container {
|
||||
border: dashed 2px rgba(0,128,0,1);
|
||||
border-radius: 10px;
|
||||
color: green;
|
||||
background-color: rgba(22,28,29,1);
|
||||
margin-left: 50%;
|
||||
width: 350px - 4px - 20px - 20px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
|
||||
p {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload_input {
|
||||
width: 0px !important;
|
||||
height: 0px !important;
|
||||
padding-top: 70px;
|
||||
padding-left: 400px;
|
||||
margin-left: 0px;
|
||||
padding-top: 10px;
|
||||
padding-left: 100%;
|
||||
padding-bottom: 10px;
|
||||
color: rgba(0,128,0,1);
|
||||
font-family: Ubuntu;
|
||||
border: dashed 2px rgba(0,128,0,1);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
|
|
@ -1332,12 +1405,56 @@ body, html {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
color: $title-color;
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.dashboard-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
|
||||
.search-input {
|
||||
width: 250px;
|
||||
padding: 8px 32px 8px 12px;
|
||||
background: $input-background;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(111,108,106,1);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $first-color;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-search {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create-button {
|
||||
|
|
@ -1363,6 +1480,7 @@ body, html {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-overlay {
|
||||
position: fixed;
|
||||
|
|
@ -1377,12 +1495,65 @@ body, html {
|
|||
z-index: 1000;
|
||||
|
||||
.form-container {
|
||||
background: $form-background;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
width: 500px;
|
||||
max-width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stylizacja natywnych suwaków */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: $form-background;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: $first-color;
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover {
|
||||
background: darken($first-color, 10%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: darken($first-color, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
background: $first-color;
|
||||
|
||||
&:hover {
|
||||
background: darken($first-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:vertical:start:decrement,
|
||||
::-webkit-scrollbar-button:vertical:end:increment,
|
||||
::-webkit-scrollbar-button:horizontal:start:decrement,
|
||||
::-webkit-scrollbar-button:horizontal:end:increment {
|
||||
background-color: $first-color;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $first-color $form-background;
|
||||
}
|
||||
|
||||
/* Style dla wyłączonych suwaków */
|
||||
::-webkit-scrollbar-thumb:disabled,
|
||||
::-webkit-scrollbar-button:disabled {
|
||||
background: rgba($first-color, 1);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Wymuszenie stylów nawet gdy content nie wymaga przewijania */
|
||||
.dashboard-content {
|
||||
min-height: 100%; /* Wymusza pojawienie się suwaka */
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue