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 components
feat/x_gpu/chat_gpt_new_version
TBS093A 2025-03-05 15:28:16 +01:00
parent e8c234ce51
commit ae781652de
28 changed files with 2839 additions and 446 deletions

1
.env 100644
View File

@ -0,0 +1 @@
REACT_APP_API_URL=http://localhost:8000

View File

@ -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
gatsby-ssr.js 100644
View File

@ -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>
);

222
package-lock.json generated
View File

@ -9,11 +9,13 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"axios": "^1.7.9", "@reduxjs/toolkit": "^2.6.0",
"axios": "^1.8.1",
"gatsby": "^5.13.3", "gatsby": "^5.13.3",
"gatsby-plugin-sass": "^6.14.0", "gatsby-plugin-sass": "^6.14.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-redux": "^9.2.0",
"react-router-dom": "^7.1.5", "react-router-dom": "^7.1.5",
"react-tsparticles": "^2.12.2", "react-tsparticles": "^2.12.2",
"sass": "^1.32.7" "sass": "^1.32.7"
@ -4130,6 +4132,40 @@
"node": ">=12" "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": { "node_modules/@sideway/address": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", "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", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=" "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": { "node_modules/@types/yoga-layout": {
"version": "1.9.2", "version": "1.9.2",
"resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz",
@ -5415,9 +5457,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.9", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
@ -9774,6 +9816,15 @@
"node": ">=18.0.0" "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": { "node_modules/gatsby-core-utils": {
"version": "4.14.0", "version": "4.14.0",
"resolved": "https://registry.npmjs.org/gatsby-core-utils/-/gatsby-core-utils-4.14.0.tgz", "resolved": "https://registry.npmjs.org/gatsby-core-utils/-/gatsby-core-utils-4.14.0.tgz",
@ -10293,6 +10344,24 @@
"webpack": "^5.59.0" "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": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "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", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" "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": { "node_modules/react-refresh": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
@ -14602,19 +14694,18 @@
} }
}, },
"node_modules/redux": { "node_modules/redux": {
"version": "4.2.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"dependencies": { "license": "MIT"
"@babel/runtime": "^7.9.2"
}
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "2.4.2", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"redux": "^4" "redux": "^5.0.0"
} }
}, },
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
@ -14850,6 +14941,12 @@
"resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz",
"integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==" "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": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -20330,6 +20436,24 @@
"config-chain": "^1.1.11" "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": { "@sideway/address": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", "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", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=" "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": { "@types/yoga-layout": {
"version": "1.9.2", "version": "1.9.2",
"resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz",
@ -21282,9 +21411,9 @@
"integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==" "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ=="
}, },
"axios": { "axios": {
"version": "1.7.9", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
"requires": { "requires": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@ -24551,6 +24680,20 @@
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"neo-async": "^2.6.1" "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", "yargs": "^15.4.1",
"yoga-layout-prebuilt": "^1.10.0", "yoga-layout-prebuilt": "^1.10.0",
"yurnalist": "^2.1.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": { "gatsby-core-utils": {
@ -27608,6 +27761,15 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" "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": { "react-refresh": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
@ -27738,17 +27900,14 @@
} }
}, },
"redux": { "redux": {
"version": "4.2.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
"requires": {
"@babel/runtime": "^7.9.2"
}
}, },
"redux-thunk": { "redux-thunk": {
"version": "2.4.2", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"requires": {} "requires": {}
}, },
"reflect.getprototypeof": { "reflect.getprototypeof": {
@ -27923,6 +28082,11 @@
"resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz",
"integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==" "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": { "resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -29420,6 +29584,12 @@
"schema-utils": "^3.0.0" "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": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -16,11 +16,13 @@
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"axios": "^1.7.9", "@reduxjs/toolkit": "^2.6.0",
"axios": "^1.8.1",
"gatsby": "^5.13.3", "gatsby": "^5.13.3",
"gatsby-plugin-sass": "^6.14.0", "gatsby-plugin-sass": "^6.14.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-redux": "^9.2.0",
"react-router-dom": "^7.1.5", "react-router-dom": "^7.1.5",
"react-tsparticles": "^2.12.2", "react-tsparticles": "^2.12.2",
"sass": "^1.32.7" "sass": "^1.32.7"

View File

@ -2,27 +2,25 @@ import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import { userAuthSelector } from '../../../redux/slices/userAuthSlice' import { userAuthSelector } from '../../../redux/slices/userAuthSlice'
import { modelCrudSelector } from '../../../redux/slices/modelCrudSlice' import { threeDModelCrudSelector } from '../../../redux/slices/threeDModelCrudSlice'
import modelCrudAsyncThunk from '../../../redux/asyncThunks/modelCrudAsyncThunk' import { uploadModel } from '../../../redux/asyncThunks/threeDModelCrudAsyncThunk'
import FormGenerator from '../formGenerator' import FormGenerator from '../formGenerator'
const ModelUploadForm = () => { const ModelUploadForm = () => {
const dispatch = useDispatch() const dispatch = useDispatch()
const [blend, setBlend] = useState('') const [blend, setBlend] = useState('')
const [blendInfo, setBlendInfo] = useState('Drop/Click\nfor upload "*.blend" file') 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 ) const { user, token } = useSelector( userAuthSelector )
let inputList = [ const inputList = [
{ {
type: 'info', type: 'info',
action: 'Upload', action: 'Upload',
endpint: 'model/upload', endpoint: 'model/upload',
button_value: 'Upload Model' button_value: 'Upload Model'
}, },
{ {
@ -37,20 +35,19 @@ const ModelUploadForm = () => {
] ]
const handleModelUpload = () => { const handleModelUpload = () => {
let body = { if (!blend) {
return;
}
dispatch( uploadModel({
user_id: user.id, user_id: user.id,
file: blend, file: blend,
token: token token: token
} }));
console.log( body )
dispatch( modelCrudAsyncThunk.fetchUploadModel( body ) )
} }
return ( return (
<div <div>
className="float_form_model"
style={ { marginTop: '17%'} }
>
<FormGenerator <FormGenerator
inputList={ inputList } inputList={ inputList }
refList={ [] } refList={ [] }

View File

@ -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

View File

@ -392,7 +392,7 @@ const DownloadFilesListInputGenerator = ({
* Text input generator, example: * Text input generator, example:
* @param { * @param {
* { * {
* type: 'chice-listing', * type: 'choice-listing',
* name: 'name', * name: 'name',
* values: list, * values: list,
* ref: React.createRef() * ref: React.createRef()
@ -488,21 +488,35 @@ const UploadInputGenerator = ({
const setDropInfos = (name, size) => { const setDropInfos = (name, size) => {
input.setDropInfo( input.setDropInfo(
'name: "' {
+ name name: name,
+ '"\nsize: ' size: (Math.round(size / 100 + 'e-2') / 100) + ' MB'
+ (Math.round(size / 100 + 'e-2') / 100) }
+ ' MB'
) )
} }
return ( return (
<div onDrop={event => onLoadFileDrop(event)} > <div
<pre style={{ marginLeft: '40px' }}> onDrop={event => onLoadFileDrop(event)}
{input.dropInfo} className='upload_input_container'
</pre> >
<p>
{
typeof input.dropInfo === 'string' ?
input.dropInfo
:
input.dropInfo.name
}
</p>
<p>
{
typeof input.dropInfo === 'string' ?
""
:
input.dropInfo.size
}
</p>
<input <input
style={{ marginTop: '-55px' }}
id={input.name + info.action + info.endpoint + 'Input'} id={input.name + info.action + info.endpoint + 'Input'}
className='upload_input' className='upload_input'
type='file' type='file'

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useMemo } from 'react';
/** /**
* Generic List Generator Component * Generic List Generator Component
@ -25,6 +25,31 @@ export const ListGenerator = ({
const [itemBeingUpdated, setItemBeingUpdated] = useState(null); const [itemBeingUpdated, setItemBeingUpdated] = useState(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [selectedItem, setSelectedItem] = useState(null); 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 // Toggle the "create" form
const handleToggleCreate = () => { const handleToggleCreate = () => {
@ -135,7 +160,7 @@ export const ListGenerator = ({
No items found. {onCreate && `Click '+ ${title || 'Item'}' to add new items.`} No items found. {onCreate && `Click '+ ${title || 'Item'}' to add new items.`}
</div> </div>
) : ( ) : (
data.map((item) => ( currentItems.map((item) => (
<div <div
key={item.id} key={item.id}
className={`item-row ${selectedItem?.id === item.id ? 'selected' : ''}`} className={`item-row ${selectedItem?.id === item.id ? 'selected' : ''}`}
@ -203,6 +228,52 @@ export const ListGenerator = ({
)} )}
</div> </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"
>
&lt;&lt;
</button>
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className="pagination-button"
>
&lt;
</button>
<span className="page-info">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="pagination-button"
>
&gt;
</button>
<button
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
className="pagination-button"
>
&gt;&gt;
</button>
</div>
</div>
)}
<div className="element-details"> <div className="element-details">
{selectedItem ? ( {selectedItem ? (
<div className="details-content"> <div className="details-content">

View File

@ -63,10 +63,10 @@ const DashboardPage = () => {
onClick={() => handleNavigation('servers')} onClick={() => handleNavigation('servers')}
> >
<i className={"fas " + icons_size + " fa-server"}></i> <i className={"fas " + icons_size + " fa-server"}></i>
<p>Dashboard</p> <p>GPU Instances</p>
</li> </li>
</ol> </ol>
<p>Rendering</p> <p>3D Stuff</p>
<ol> <ol>
<li <li
className={isActive('3d-models') ? 'active' : ''} className={isActive('3d-models') ? 'active' : ''}
@ -80,10 +80,10 @@ const DashboardPage = () => {
onClick={() => handleNavigation('renders')} onClick={() => handleNavigation('renders')}
> >
<i className={"fas " + icons_size + " fa-paint-brush"}></i> <i className={"fas " + icons_size + " fa-paint-brush"}></i>
<p>Rendered Materials</p> <p>3D Rendering</p>
</li> </li>
</ol> </ol>
<p>AI Training</p> <p>AI Stuff</p>
<ol> <ol>
<li <li
className={isActive('ai-models') ? 'active' : ''} className={isActive('ai-models') ? 'active' : ''}
@ -97,7 +97,7 @@ const DashboardPage = () => {
onClick={() => handleNavigation('ai-tasks')} onClick={() => handleNavigation('ai-tasks')}
> >
<i className={"fas " + icons_size + " fa-microchip"}></i> <i className={"fas " + icons_size + " fa-microchip"}></i>
<p>AI Training Tasks</p> <p>AI Training</p>
</li> </li>
</ol> </ol>
<p>User</p> <p>User</p>

View File

@ -1,31 +1,244 @@
import React, { useState } from 'react'; import React, { useState, useMemo } from 'react';
import { ListGenerator } from '../../components/forms/listGenerator'; import { ListGenerator } from '../../components/forms/listGenerator';
import { FormGenerator } from '../../components/forms/formGenerator'; import ModelUploadForm from '../../components/forms/3d_model_crud/threeDModelUpload';
const ThreeDModelsDashboard = () => { const ThreeDModelsDashboard = () => {
const [selectedModel, setSelectedModel] = useState(null); const [selectedModel, setSelectedModel] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [models, setModels] = useState([ const [models, setModels] = useState([
{ {
id: 1, id: 1,
name: 'Model A', name: 'Dragon Model',
type: '3D', type: 'Blender',
status: 'Active', status: 'Active',
lastModified: '2024-03-20', lastModified: '2024-03-20',
size: '2.5MB' version: '2.1'
}, },
{ {
id: 2, id: 2,
name: 'Model B', name: 'Medieval Castle',
type: '3D', type: 'Maya',
status: 'Inactive', status: 'Inactive',
lastModified: '2024-03-19', 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 [isFormVisible, setIsFormVisible] = useState(false);
const [formMode, setFormMode] = useState('create'); // 'create' lub 'edit' const [formMode, setFormMode] = useState('create');
const [message, setMessage] = useState({ type: '', text: '' }); 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) => { const handleModelSelect = (model) => {
setSelectedModel(model); setSelectedModel(model);
}; };
@ -60,28 +273,7 @@ const ThreeDModelsDashboard = () => {
if (selectedModel?.id === modelId) { if (selectedModel?.id === modelId) {
setSelectedModel(null); setSelectedModel(null);
} }
setMessage({ type: 'success', text: 'Model has been deleted' }); setMessage({ type: 'success', text: '3D 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);
}; };
const handleFormCancel = () => { const handleFormCancel = () => {
@ -104,50 +296,45 @@ const ThreeDModelsDashboard = () => {
]; ];
}; };
const formFields = [ const filteredModels = useMemo(() => {
{ return models.filter(model =>
name: 'name', model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
label: 'Model Name', model.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
type: 'text', model.status.toLowerCase().includes(searchQuery.toLowerCase())
required: true, );
value: selectedModel?.name || '' }, [models, searchQuery]);
},
{
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'
}
];
return ( return (
<div className="list-container"> <div className="list-container">
<div className="dashboard-header"> <div className="dashboard-header">
<h2>3D Models</h2> <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 <button
className="create-button" className="create-button"
onClick={handleCreateModel} onClick={handleCreateModel}
> >
<i className="fas fa-plus"></i> <i className="fas fa-upload"></i>
3D Model Upload 3D Model
</button> </button>
</div> </div>
</div>
{message.text && ( {message.text && (
<div className={`message ${message.type}`}> <div className={`message ${message.type}`}>
@ -158,18 +345,13 @@ const ThreeDModelsDashboard = () => {
{isFormVisible && ( {isFormVisible && (
<div className="form-overlay"> <div className="form-overlay">
<div className="form-container"> <div className="form-container">
<FormGenerator <ModelUploadForm/>
fields={formFields}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
title={formMode === 'create' ? 'Create new model' : 'Edit model'}
/>
</div> </div>
</div> </div>
)} )}
<ListGenerator <ListGenerator
data={models} data={filteredModels}
selectedItem={selectedModel} selectedItem={selectedModel}
onItemSelect={handleModelSelect} onItemSelect={handleModelSelect}
onItemAction={handleModelAction} onItemAction={handleModelAction}
@ -184,26 +366,30 @@ const ThreeDModelsDashboard = () => {
</div> </div>
</div> </div>
<div className="item-details"> <div className="item-details">
<div>Version: {model.version}</div>
<div>Last Modified: {model.lastModified}</div> <div>Last Modified: {model.lastModified}</div>
<div>Size: {model.size}</div>
</div> </div>
</div> </div>
)} )}
renderDetails={(model) => ( renderDetails={(model) => (
<div className="details-panel"> <div className="details-panel">
<h3>Model Details</h3> <h3>3D Model Details</h3>
<div className="detail-row"> <div className="detail-row">
<span className="detail-label">ID:</span> <span className="detail-label">ID:</span>
<span className="detail-value">{model.id}</span> <span className="detail-value">{model.id}</span>
</div> </div>
<div className="detail-row"> <div className="detail-row">
<span className="detail-label">Nazwa:</span> <span className="detail-label">Name:</span>
<span className="detail-value">{model.name}</span> <span className="detail-value">{model.name}</span>
</div> </div>
<div className="detail-row"> <div className="detail-row">
<span className="detail-label">Typ:</span> <span className="detail-label">Type:</span>
<span className="detail-value">{model.type}</span> <span className="detail-value">{model.type}</span>
</div> </div>
<div className="detail-row">
<span className="detail-label">Version:</span>
<span className="detail-value">{model.version}</span>
</div>
<div className="detail-row"> <div className="detail-row">
<span className="detail-label">Status:</span> <span className="detail-label">Status:</span>
<span className="detail-value">{model.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-label">Last Modified:</span>
<span className="detail-value">{model.lastModified}</span> <span className="detail-value">{model.lastModified}</span>
</div> </div>
<div className="detail-row">
<span className="detail-label">Size:</span>
<span className="detail-value">{model.size}</span>
</div>
</div> </div>
)} )}
/> />

View File

@ -1,9 +1,11 @@
import React, { useState } from 'react'; import React, { useState, useRef, useMemo } from 'react';
import { ListGenerator } from '../../components/forms/listGenerator'; import { ListGenerator } from '../../components/forms/listGenerator';
import FormGenerator from '../../components/forms/formGenerator'; import FormGenerator from '../../components/forms/formGenerator';
import AIModelUploadForm from '../../components/forms/ai_model_crud/AIModelUpload';
const AIModelsDashboard = () => { const AIModelsDashboard = () => {
const [selectedModel, setSelectedModel] = useState(null); const [selectedModel, setSelectedModel] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [models, setModels] = useState([ const [models, setModels] = useState([
{ {
id: 1, id: 1,
@ -20,12 +22,178 @@ const AIModelsDashboard = () => {
status: 'Inactive', status: 'Inactive',
lastModified: '2024-03-19', lastModified: '2024-03-19',
version: '1.0' 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 [isFormVisible, setIsFormVisible] = useState(false);
const [formMode, setFormMode] = useState('create'); const [formMode, setFormMode] = useState('create');
const [message, setMessage] = useState({ type: '', text: '' }); 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) => { const handleModelSelect = (model) => {
setSelectedModel(model); setSelectedModel(model);
}; };
@ -104,57 +272,45 @@ const AIModelsDashboard = () => {
]; ];
}; };
const formFields = [ const filteredModels = useMemo(() => {
{ return models.filter(model =>
name: 'name', model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
label: 'Model Name', model.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
type: 'text', model.status.toLowerCase().includes(searchQuery.toLowerCase())
required: true, );
value: selectedModel?.name || '' }, [models, searchQuery]);
},
{
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'
}
];
return ( return (
<div className="list-container"> <div className="list-container">
<div className="dashboard-header"> <div className="dashboard-header">
<h2>AI Models</h2> <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 <button
className="create-button" className="create-button"
onClick={handleCreateModel} onClick={() => setIsFormVisible(true)}
> >
<i className="fas fa-plus"></i> <i className="fas fa-upload"></i>
AI Model Upload AI Model
</button> </button>
</div> </div>
</div>
{message.text && ( {message.text && (
<div className={`message ${message.type}`}> <div className={`message ${message.type}`}>
@ -165,18 +321,13 @@ const AIModelsDashboard = () => {
{isFormVisible && ( {isFormVisible && (
<div className="form-overlay"> <div className="form-overlay">
<div className="form-container"> <div className="form-container">
<FormGenerator <AIModelUploadForm />
fields={formFields}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
title={formMode === 'create' ? 'Create new AI Model' : 'Edit AI Model'}
/>
</div> </div>
</div> </div>
)} )}
<ListGenerator <ListGenerator
data={models} data={filteredModels}
selectedItem={selectedModel} selectedItem={selectedModel}
onItemSelect={handleModelSelect} onItemSelect={handleModelSelect}
onItemAction={handleModelAction} onItemAction={handleModelAction}
@ -204,15 +355,15 @@ const AIModelsDashboard = () => {
<span className="detail-value">{model.id}</span> <span className="detail-value">{model.id}</span>
</div> </div>
<div className="detail-row"> <div className="detail-row">
<span className="detail-label">Nazwa:</span> <span className="detail-label">Name:</span>
<span className="detail-value">{model.name}</span> <span className="detail-value">{model.name}</span>
</div> </div>
<div className="detail-row"> <div className="detail-row">
<span className="detail-label">Typ:</span> <span className="detail-label">Type:</span>
<span className="detail-value">{model.type}</span> <span className="detail-value">{model.type}</span>
</div> </div>
<div className="detail-row"> <div className="detail-row">
<span className="detail-label">Wersja:</span> <span className="detail-label">Version:</span>
<span className="detail-value">{model.version}</span> <span className="detail-value">{model.version}</span>
</div> </div>
<div className="detail-row"> <div className="detail-row">
@ -220,7 +371,7 @@ const AIModelsDashboard = () => {
<span className="detail-value">{model.status}</span> <span className="detail-value">{model.status}</span>
</div> </div>
<div className="detail-row"> <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> <span className="detail-value">{model.lastModified}</span>
</div> </div>
</div> </div>

View File

@ -1,31 +1,232 @@
import React, { useState } from 'react'; import React, { useState, useRef, useMemo } from 'react';
import { ListGenerator } from '../../components/forms/listGenerator'; import { ListGenerator } from '../../components/forms/listGenerator';
import { FormGenerator } from '../../components/forms/formGenerator'; import FormGenerator from '../../components/forms/formGenerator';
const AITasksDashboard = () => { const AITasksDashboard = () => {
const [selectedTask, setSelectedTask] = useState(null); const [selectedTask, setSelectedTask] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [tasks, setTasks] = useState([ const [tasks, setTasks] = useState([
{ {
id: 1, id: 1,
name: 'Task A', name: "Model Training - CNN",
type: 'Training', type: "Training",
status: 'In Progress', status: "Completed",
lastModified: '2024-03-20', progress: 100,
progress: 45 model: "Object Detection v2",
startTime: "2024-03-20 09:00:00",
endTime: "2024-03-20 14:30:00"
}, },
{ {
id: 2, id: 2,
name: 'Task B', name: "BERT Fine-tuning",
type: 'Inference', type: "Fine-tuning",
status: 'Completed', status: "In Progress",
lastModified: '2024-03-19', progress: 75,
progress: 100 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 [isFormVisible, setIsFormVisible] = useState(false);
const [formMode, setFormMode] = useState('create'); const [formMode, setFormMode] = useState('create');
const [message, setMessage] = useState({ type: '', text: '' }); 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) => { const handleTaskSelect = (task) => {
setSelectedTask(task); setSelectedTask(task);
}; };
@ -114,6 +315,14 @@ const AITasksDashboard = () => {
setSelectedTask(null); setSelectedTask(null);
}; };
const handleFormAction = (refs) => {
const formData = {};
refs.forEach((ref, index) => {
formData[inputList[index].name] = ref.current.value;
});
handleFormSubmit(formData);
};
const getTaskActions = (task) => { const getTaskActions = (task) => {
const actions = []; const actions = [];
@ -147,53 +356,46 @@ const AITasksDashboard = () => {
return actions; return actions;
}; };
const formFields = [ const filteredTasks = useMemo(() => {
{ return tasks.filter(task =>
name: 'name', task.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
label: 'Task Name', task.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
type: 'text', task.status.toLowerCase().includes(searchQuery.toLowerCase()) ||
required: true, task.model.toLowerCase().includes(searchQuery.toLowerCase())
value: selectedTask?.name || '' );
}, }, [tasks, searchQuery]);
{
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'
}
];
return ( return (
<div className="list-container"> <div className="list-container">
<div className="dashboard-header"> <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 <button
className="create-button" className="create-button"
onClick={handleCreateTask} onClick={handleCreateTask}
> >
<i className="fas fa-plus"></i> <i className="fas fa-plus"></i>
Task Create Task
</button> </button>
</div> </div>
</div>
{message.text && ( {message.text && (
<div className={`message ${message.type}`}> <div className={`message ${message.type}`}>
@ -205,17 +407,17 @@ const AITasksDashboard = () => {
<div className="form-overlay"> <div className="form-overlay">
<div className="form-container"> <div className="form-container">
<FormGenerator <FormGenerator
fields={formFields} inputList={inputList}
formRefs={formRefs}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
onCancel={handleFormCancel} onCancel={handleFormCancel}
title={formMode === 'create' ? 'Create new task' : 'Edit task'}
/> />
</div> </div>
</div> </div>
)} )}
<ListGenerator <ListGenerator
data={tasks} data={filteredTasks}
selectedItem={selectedTask} selectedItem={selectedTask}
onItemSelect={handleTaskSelect} onItemSelect={handleTaskSelect}
onItemAction={handleTaskAction} onItemAction={handleTaskAction}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useRef, useMemo } from 'react';
import { ListGenerator } from '../../components/forms/listGenerator'; import { ListGenerator } from '../../components/forms/listGenerator';
import FormGenerator from '../../components/forms/formGenerator'; import FormGenerator from '../../components/forms/formGenerator';
@ -7,26 +7,205 @@ const RendersDashboard = () => {
const [renders, setRenders] = useState([ const [renders, setRenders] = useState([
{ {
id: 1, id: 1,
name: 'Render A', name: "Character Animation",
type: '3D', type: "Animation",
status: 'Completed', status: "Completed",
lastModified: '2024-03-20', progress: 100,
resolution: '1920x1080', model: "Hero Character",
progress: 100 startTime: "2024-03-20 09:00:00",
endTime: "2024-03-20 11:30:00"
}, },
{ {
id: 2, id: 2,
name: 'Render B', name: "Environment Lighting",
type: '2D', type: "Still",
status: 'In Progress', status: "In Progress",
lastModified: '2024-03-19', progress: 75,
resolution: '3840x2160', model: "Forest Scene",
progress: 45 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 [isFormVisible, setIsFormVisible] = useState(false);
const [formMode, setFormMode] = useState('create'); const [formMode, setFormMode] = useState('create');
const [message, setMessage] = useState({ type: '', text: '' }); 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) => { const handleRenderSelect = (render) => {
setSelectedRender(render); setSelectedRender(render);
@ -107,72 +286,199 @@ const RendersDashboard = () => {
]; ];
}; };
const formFields = [ const mockRenders = [
{ {
name: 'name', id: 1,
label: 'Nazwa renderu', name: "Dragon Scene",
type: 'text', type: "Animation",
required: true, status: "Completed",
value: selectedRender?.name || '' progress: 100,
model: "Dragon Model",
startTime: "2024-03-01 10:00:00",
endTime: "2024-03-01 12:30:00"
}, },
{ {
name: 'type', id: 2,
label: 'Typ renderu', name: "Castle Exterior",
type: 'select', type: "Still",
required: true, status: "In Progress",
options: [ progress: 65,
{ value: '3D', label: '3D' }, model: "Medieval Castle",
{ value: '2D', label: '2D' } startTime: "2024-03-02 09:00:00",
], endTime: null
value: selectedRender?.type || '3D'
}, },
{ {
name: 'resolution', id: 3,
label: 'Rozdzielczość', name: "Weapon Showcase",
type: 'select', type: "360 View",
required: true, status: "Queued",
options: [ progress: 0,
{ value: '1920x1080', label: 'Full HD (1920x1080)' }, model: "Sci-fi Weapon",
{ value: '3840x2160', label: '4K (3840x2160)' }, startTime: null,
{ value: '7680x4320', label: '8K (7680x4320)' } endTime: null
],
value: selectedRender?.resolution || '1920x1080'
}, },
{ {
name: 'status', id: 4,
label: 'Status', name: "Forest Flythrough",
type: 'select', type: "Animation",
required: true, status: "Completed",
options: [ progress: 100,
{ value: 'In Progress', label: 'W trakcie' }, model: "Forest Scene",
{ value: 'Completed', label: 'Zakończony' }, startTime: "2024-03-01 14:00:00",
{ value: 'Failed', label: 'Nieudany' } endTime: "2024-03-01 16:00:00"
],
value: selectedRender?.status || 'In Progress'
}, },
{ {
name: 'progress', id: 5,
label: 'Postęp', name: "Robot Animation",
type: 'number', type: "Animation",
required: true, status: "In Progress",
min: 0, progress: 45,
max: 100, model: "Robot Character",
value: selectedRender?.progress || 0 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 ( return (
<div className="list-container"> <div className="list-container">
<div className="dashboard-header"> <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 <button
className="create-button" className="create-button"
onClick={handleCreateRender} onClick={handleCreateRender}
> >
<i className="fas fa-plus"></i> <i className="fas fa-plus"></i>
Render Create Render
</button> </button>
</div> </div>
</div>
{message.text && ( {message.text && (
<div className={`message ${message.type}`}> <div className={`message ${message.type}`}>
@ -184,17 +490,16 @@ const RendersDashboard = () => {
<div className="form-overlay"> <div className="form-overlay">
<div className="form-container"> <div className="form-container">
<FormGenerator <FormGenerator
fields={formFields} inputList={inputList}
onSubmit={handleFormSubmit} refList={formRefs}
onCancel={handleFormCancel} action={handleFormSubmit}
title={formMode === 'create' ? 'Utwórz nowy render' : 'Edytuj render'}
/> />
</div> </div>
</div> </div>
)} )}
<ListGenerator <ListGenerator
data={renders} data={filteredRenders}
selectedItem={selectedRender} selectedItem={selectedRender}
onItemSelect={handleRenderSelect} onItemSelect={handleRenderSelect}
onItemAction={handleRenderAction} onItemAction={handleRenderAction}

View File

@ -1,31 +1,214 @@
import React, { useState } from 'react'; import React, { useState, useRef, useMemo } from 'react';
import { ListGenerator } from '../../components/forms/listGenerator'; import { ListGenerator } from '../../components/forms/listGenerator';
import FormGenerator from '../../components/forms/formGenerator'; import FormGenerator from '../../components/forms/formGenerator';
const ServersDashboard = () => { const ServersDashboard = () => {
const [selectedServer, setSelectedServer] = useState(null); const [selectedServer, setSelectedServer] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [servers, setServers] = useState([ const [servers, setServers] = useState([
{ {
id: 1, id: 1,
name: 'Server A', name: "Render Node 1",
type: 'Render', type: "Render Node",
status: 'Online', status: "Active",
lastModified: '2024-03-20', progress: 100,
ip: '192.168.1.100' ip: "192.168.1.101",
lastActive: "2024-03-20 11:30:00"
}, },
{ {
id: 2, id: 2,
name: 'Server B', name: "AI Training Node 1",
type: 'AI', type: "AI Training Node",
status: 'Offline', status: "In Progress",
lastModified: '2024-03-19', progress: 75,
ip: '192.168.1.101' 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 [isFormVisible, setIsFormVisible] = useState(false);
const [formMode, setFormMode] = useState('create'); const [formMode, setFormMode] = useState('create');
const [message, setMessage] = useState({ type: '', text: '' }); 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) => { const handleServerSelect = (server) => {
setSelectedServer(server); setSelectedServer(server);
}; };
@ -104,59 +287,54 @@ const ServersDashboard = () => {
]; ];
}; };
const formFields = [ const handleFormAction = (refs) => {
{ const formData = {};
name: 'name', refs.forEach((ref, index) => {
label: 'Nazwa serwera', formData[inputList[index].name] = ref.current.value;
type: 'text', });
required: true, handleFormSubmit(formData);
value: selectedServer?.name || '' };
},
{ const filteredServers = useMemo(() => {
name: 'type', return servers.filter(server =>
label: 'Typ serwera', server.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
type: 'select', server.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
required: true, server.status.toLowerCase().includes(searchQuery.toLowerCase()) ||
options: [ server.ip.toLowerCase().includes(searchQuery.toLowerCase())
{ value: 'Render', label: 'Render' }, );
{ value: 'AI', label: 'AI' }, }, [servers, searchQuery]);
{ 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'
}
];
return ( return (
<div className="list-container"> <div className="list-container">
<div className="dashboard-header"> <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 <button
className="create-button" className="create-button"
onClick={handleCreateServer} onClick={handleCreateServer}
> >
<i className="fas fa-plus"></i> <i className="fas fa-plus"></i>
Server Add Server
</button> </button>
</div> </div>
</div>
{message.text && ( {message.text && (
<div className={`message ${message.type}`}> <div className={`message ${message.type}`}>
@ -168,17 +346,16 @@ const ServersDashboard = () => {
<div className="form-overlay"> <div className="form-overlay">
<div className="form-container"> <div className="form-container">
<FormGenerator <FormGenerator
fields={formFields} inputList={inputList}
onSubmit={handleFormSubmit} refList={formRefs}
onCancel={handleFormCancel} action={handleFormAction}
title={formMode === 'create' ? 'Utwórz nowy serwer' : 'Edytuj serwer'}
/> />
</div> </div>
</div> </div>
)} )}
<ListGenerator <ListGenerator
data={servers} data={filteredServers}
selectedItem={selectedServer} selectedItem={selectedServer}
onItemSelect={handleServerSelect} onItemSelect={handleServerSelect}
onItemAction={handleServerAction} onItemAction={handleServerAction}

View File

@ -138,6 +138,16 @@ const UserSettings = () => {
return baseInputs; 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 ( return (
<div className="list-container"> <div className="list-container">
<div className="user-settings"> <div className="user-settings">
@ -157,19 +167,26 @@ const UserSettings = () => {
</div> </div>
)} )}
{isEditing && (
<div className="form-overlay">
<div className="form-container">
<FormGenerator <FormGenerator
inputList={getInputList()} inputList={getInputList().map((field, index) => ({
refList={[ ...field,
usernameInput, ref: formRefs[index],
emailInput, type: field.type,
currentPasswordInput, name: field.name,
newPasswordInput, onChange: field.onChange,
confirmPasswordInput validationInfo: field.validationInfo
]} }))}
action={handleSubmit} refList={formRefs}
action={handleFormAction}
/> />
</div> </div>
</div> </div>
)}
</div>
</div>
); );
}; };

11
src/pages/index.js 100644
View File

@ -0,0 +1,11 @@
import React from 'react';
const IndexPage = () => {
return (
<div>
{/* Reszta komponentów aplikacji */}
</div>
);
};
export default IndexPage;

View File

@ -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;
}
);

View File

@ -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;
}
);

View File

@ -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;
}
);

View File

@ -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;
}
);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

14
src/redux/store.js 100644
View File

@ -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
}
});

View File

@ -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 { .list-row {
@ -1046,25 +1115,29 @@ body, html {
} }
} }
.float_form_model { .upload_input_container {
position: fixed; border: dashed 2px rgba(0,128,0,1);
width: 400px;
padding: 50px;
border-radius: 10px; border-radius: 10px;
color: green; width: 350px - 4px - 20px - 20px;
background-color: rgba(22,28,29,1); padding-left: 20px;
margin-left: 50%; padding-right: 20px;
p {
font-weight: bold;
font-size: 12px;
text-align: center;
}
.upload_input { .upload_input {
width: 0px !important; width: 0px !important;
height: 0px !important; height: 0px !important;
padding-top: 70px; padding-top: 10px;
padding-left: 400px; padding-left: 100%;
margin-left: 0px; padding-bottom: 10px;
color: rgba(0,128,0,1); color: rgba(0,128,0,1);
font-family: Ubuntu; font-family: Ubuntu;
border: dashed 2px rgba(0,128,0,1);
overflow: hidden; overflow: hidden;
cursor: pointer;
} }
button { button {
@ -1332,12 +1405,56 @@ body, html {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px; margin-bottom: 20px;
h2 { h2 {
color: $title-color;
margin: 0; 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 { .create-button {
@ -1363,6 +1480,7 @@ body, html {
} }
} }
} }
}
.form-overlay { .form-overlay {
position: fixed; position: fixed;
@ -1377,12 +1495,65 @@ body, html {
z-index: 1000; z-index: 1000;
.form-container { .form-container {
background: $form-background;
padding: 30px;
border-radius: 10px;
width: 500px;
max-width: 90%; max-width: 90%;
max-height: 90vh; max-height: 90vh;
overflow-y: auto; 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 */
}