mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
Compare commits
35 Commits
v4.2.1
...
feat/separ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe0ec82a3d | ||
|
|
419c794e03 | ||
|
|
bbb02e991c | ||
|
|
8f78b3f1ca | ||
|
|
a1d02b486a | ||
|
|
cc85fba207 | ||
|
|
5958d33fce | ||
|
|
3a20930ead | ||
|
|
bf81a63f8e | ||
|
|
7e6be67f61 | ||
|
|
6a92f61f1a | ||
|
|
9cad1a9454 | ||
|
|
2c01ba9610 | ||
|
|
9ce10a72b2 | ||
|
|
f3e6a0011e | ||
|
|
cb2020dee6 | ||
|
|
db189abec4 | ||
|
|
d8afc8f4c9 | ||
|
|
3bfcc8e8c0 | ||
|
|
1892e23c22 | ||
|
|
03ece335b8 | ||
|
|
7bc9949110 | ||
|
|
ad3906e682 | ||
|
|
a356bf03fb | ||
|
|
57a6c49f8a | ||
|
|
9e54237670 | ||
|
|
f56d6ce5a2 | ||
|
|
590ab7327f | ||
|
|
9d63e56374 | ||
|
|
afa6087e95 | ||
|
|
c9789ac1f2 | ||
|
|
14ff3398ba | ||
|
|
6c042cbe01 | ||
|
|
3380929c04 | ||
|
|
bd69b3383f |
9
.github/workflows/main.yml
vendored
9
.github/workflows/main.yml
vendored
@@ -399,8 +399,13 @@ jobs:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
run: |
|
||||
# Sync the deploy directory to the Cloudflare bucket - CRC32 is required for the checksum-algorithm on cloudflare (ref. https://community.cloudflare.com/t/an-error-occurred-internalerror-when-calling-the-putobject-operation/764905/8)
|
||||
aws s3 sync deploy/ s3://${{ secrets.CF_BUCKET_PREVIEW }}/${{ steps.build-plugin.outputs.BUCKET_PATH }} --endpoint-url ${{ secrets.CF_ENDPOINT }} --checksum-algorithm CRC32
|
||||
# Sync the deploy directory to the Cloudflare bucket with explicit content encoding and public-read ACL
|
||||
aws s3 sync deploy/ s3://${{ secrets.CF_BUCKET_PREVIEW }}/${{ steps.build-plugin.outputs.BUCKET_PATH }} \
|
||||
--endpoint-url ${{ secrets.CF_ENDPOINT }} \
|
||||
--checksum-algorithm CRC32 \
|
||||
--no-guess-mime-type \
|
||||
--content-encoding none \
|
||||
--acl public-read
|
||||
|
||||
- name: Upload Release Assets
|
||||
if: needs.release-please.outputs.releases_created == 'true'
|
||||
|
||||
33
.github/workflows/release-production.yml
vendored
33
.github/workflows/release-production.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
prerelease: false
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
node-version: '22.x'
|
||||
- run: npm install html-escaper@2 xml2js
|
||||
- name: Update Plugin Changelog
|
||||
uses: actions/github-script@v7
|
||||
@@ -66,8 +66,23 @@ jobs:
|
||||
|
||||
// Validate the plugin file is valid XML
|
||||
const xml2js = require('xml2js');
|
||||
const parser = new xml2js.Parser();
|
||||
const parser = new xml2js.Parser({
|
||||
explicitCharkey: true,
|
||||
trim: true,
|
||||
explicitRoot: true,
|
||||
explicitArray: false,
|
||||
attrkey: 'ATTR',
|
||||
charkey: 'TEXT',
|
||||
xmlnskey: 'XMLNS',
|
||||
normalizeTags: false,
|
||||
normalize: false,
|
||||
strict: false // Try with less strict parsing
|
||||
});
|
||||
parser.parseStringPromise(pluginContent).then((result) => {
|
||||
if (!result) {
|
||||
console.error('Plugin file is not valid XML');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Plugin file is valid XML');
|
||||
|
||||
// Write back to file
|
||||
@@ -84,7 +99,12 @@ jobs:
|
||||
AWS_DEFAULT_REGION: ${{ secrets.DO_SPACE_REGION }}
|
||||
AWS_ENDPOINT_URL: https://${{ secrets.DO_SPACE_REGION }}.digitaloceanspaces.com
|
||||
run: |
|
||||
aws s3 sync . s3://${{ secrets.DO_SPACE_NAME }}/unraid-api --checksum-algorithm CRC32
|
||||
# Upload files with explicit content encoding and public-read ACL
|
||||
aws s3 sync . s3://${{ secrets.DO_SPACE_NAME }}/unraid-api \
|
||||
--checksum-algorithm CRC32 \
|
||||
--no-guess-mime-type \
|
||||
--content-encoding none \
|
||||
--acl public-read
|
||||
|
||||
- name: Upload Release Files to Cloudflare Bucket
|
||||
env:
|
||||
@@ -93,4 +113,9 @@ jobs:
|
||||
AWS_DEFAULT_REGION: auto
|
||||
AWS_ENDPOINT_URL: ${{ secrets.CF_ENDPOINT }}
|
||||
run: |
|
||||
aws s3 sync . s3://${{ secrets.CF_BUCKET }}/unraid-api --checksum-algorithm CRC32
|
||||
# Upload files with explicit content encoding and public-read ACL
|
||||
aws s3 sync . s3://${{ secrets.CF_BUCKET }}/unraid-api \
|
||||
--checksum-algorithm CRC32 \
|
||||
--no-guess-mime-type \
|
||||
--content-encoding none \
|
||||
--acl public-read
|
||||
|
||||
@@ -1 +1 @@
|
||||
{".":"4.2.1"}
|
||||
{".":"4.4.0"}
|
||||
|
||||
@@ -1,5 +1,51 @@
|
||||
# Changelog
|
||||
|
||||
## [4.4.0](https://github.com/unraid/api/compare/v4.3.1...v4.4.0) (2025-03-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ReplaceKey functionality to plugin ([#1264](https://github.com/unraid/api/issues/1264)) ([4aadcef](https://github.com/unraid/api/commit/4aadcef1ca6b45b44885f2d2a986874e86945d4f))
|
||||
* downgrade page replace key check ([#1263](https://github.com/unraid/api/issues/1263)) ([8d56d12](https://github.com/unraid/api/commit/8d56d12f67d86d7015a358727bcb303eb511ac42))
|
||||
* make log viewer component dynamic ([#1242](https://github.com/unraid/api/issues/1242)) ([e6ec110](https://github.com/unraid/api/commit/e6ec110fbf81b329b72ef350643bf3c76734290a))
|
||||
* ReplaceKey functionality in Registration and Update pages ([#1246](https://github.com/unraid/api/issues/1246)) ([04307c9](https://github.com/unraid/api/commit/04307c977cfd4916753140e5a20811c561d2dfb2))
|
||||
* UnraidCheckExec for Check OS Updates via UPC dropdown ([#1265](https://github.com/unraid/api/issues/1265)) ([5935a3b](https://github.com/unraid/api/commit/5935a3b3c2f69ee683146c3fcc798d72996633f8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update all non-major dependencies ([#1236](https://github.com/unraid/api/issues/1236)) ([7194f85](https://github.com/unraid/api/commit/7194f859ce0178116621b4bdf28db553b037940d))
|
||||
* **deps:** update all non-major dependencies ([#1247](https://github.com/unraid/api/issues/1247)) ([20b0aeb](https://github.com/unraid/api/commit/20b0aeb9d7e621ec917c928135b2c867e07ce7a4))
|
||||
* **deps:** update all non-major dependencies ([#1251](https://github.com/unraid/api/issues/1251)) ([33a1a1d](https://github.com/unraid/api/commit/33a1a1ddd2f228cf001bb492f9c76bb5bc6dc8a0))
|
||||
* **deps:** update all non-major dependencies ([#1253](https://github.com/unraid/api/issues/1253)) ([53fec0e](https://github.com/unraid/api/commit/53fec0efaba8f3e2dcf5f2899e3099e0e12f5162))
|
||||
* **deps:** update dependency @nestjs/passport to v11 ([#1244](https://github.com/unraid/api/issues/1244)) ([edc93a9](https://github.com/unraid/api/commit/edc93a921ea93d98b7c2de9a7fed2fed650365e8))
|
||||
* **deps:** update dependency graphql-subscriptions to v3 ([#1209](https://github.com/unraid/api/issues/1209)) ([c14c85f](https://github.com/unraid/api/commit/c14c85fcf7ce920edd75d15fa9b3d556f452bb88))
|
||||
* **deps:** update dependency ini to v5 ([#1217](https://github.com/unraid/api/issues/1217)) ([f27660f](https://github.com/unraid/api/commit/f27660f140acb647cab1dce162af0d49d5655fb6))
|
||||
* **deps:** update dependency jose to v6 ([#1248](https://github.com/unraid/api/issues/1248)) ([42e3d59](https://github.com/unraid/api/commit/42e3d59107dd800351ee1f7d175c550465ebddb4))
|
||||
* **deps:** update dependency marked to v15 ([#1249](https://github.com/unraid/api/issues/1249)) ([2b6693f](https://github.com/unraid/api/commit/2b6693f404a9a3405300637d2c55bd5b65c61f0b))
|
||||
* **deps:** update dependency pino-pretty to v13 ([#1250](https://github.com/unraid/api/issues/1250)) ([85fb910](https://github.com/unraid/api/commit/85fb91059a0ad7728d766cd3b429857b3fd4bd08))
|
||||
* **deps:** update dependency pm2 to v6 ([#1258](https://github.com/unraid/api/issues/1258)) ([04ad2bc](https://github.com/unraid/api/commit/04ad2bc9c8c1e5fd9622655ce9881bde17388246))
|
||||
* **deps:** update dependency shadcn-vue to v1 ([#1259](https://github.com/unraid/api/issues/1259)) ([1a4fe8f](https://github.com/unraid/api/commit/1a4fe8f85f50cf34df6af2c0bf55efc305fa9fff))
|
||||
* **deps:** update dependency vue-i18n to v11 ([#1261](https://github.com/unraid/api/issues/1261)) ([0063286](https://github.com/unraid/api/commit/0063286e29b9a7439e6acc64973a3152370c75c3))
|
||||
* **deps:** update vueuse monorepo to v13 (major) ([#1262](https://github.com/unraid/api/issues/1262)) ([94caae3](https://github.com/unraid/api/commit/94caae3d87fbfb04dfe5633030e24acddbcc0f6b))
|
||||
* make scripts executable when building the plugin ([#1255](https://github.com/unraid/api/issues/1255)) ([e237f38](https://github.com/unraid/api/commit/e237f38bc4646f13461938c03d566fef781ad406))
|
||||
* node installation not persisting across reboots ([#1256](https://github.com/unraid/api/issues/1256)) ([0415cf1](https://github.com/unraid/api/commit/0415cf1252ed8c7fba32a65c031dc0d21e0f5a81))
|
||||
* update configValid state to ineligible in var.ini and adjust rel… ([#1268](https://github.com/unraid/api/issues/1268)) ([ef8c954](https://github.com/unraid/api/commit/ef8c9548baef99010e2f26288af33a90167e5177))
|
||||
|
||||
## [4.3.1](https://github.com/unraid/api/compare/v4.3.0...v4.3.1) (2025-03-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* stepper fixes ([#1240](https://github.com/unraid/api/issues/1240)) ([e7f6f5e](https://github.com/unraid/api/commit/e7f6f5e8315c50fd37193f7d7de2af3d370c18ea))
|
||||
|
||||
## [4.3.0](https://github.com/unraid/api/compare/v4.2.1...v4.3.0) (2025-03-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* update production release flow to validate less strictly ([#1238](https://github.com/unraid/api/issues/1238)) ([3afb203](https://github.com/unraid/api/commit/3afb203b856655e4ce7c9d709e30d3f2ebd64784))
|
||||
|
||||
## [4.2.1](https://github.com/unraid/api/compare/v4.2.0...v4.2.1) (2025-03-18)
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ shareAvahiSMBModel="Xserve"
|
||||
shfs_logging="1"
|
||||
safeMode="no"
|
||||
startMode="Normal"
|
||||
configValid="yes"
|
||||
configValid="ineligible"
|
||||
joinStatus="Not joined"
|
||||
deviceCount="4"
|
||||
flashGUID="0000-0000-0000-000000000000"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/api",
|
||||
"version": "4.2.1",
|
||||
"version": "4.4.0",
|
||||
"main": "src/cli/index.ts",
|
||||
"type": "module",
|
||||
"corepack": {
|
||||
@@ -18,7 +18,9 @@
|
||||
"dev": "vite",
|
||||
"command": "pnpm run build && clear && ./dist/cli.js",
|
||||
"// Build and Deploy": "",
|
||||
"build": "vite build --mode=production",
|
||||
"build": "pnpm run build:connect",
|
||||
"build:connect": "CONNECT=true vite build --mode=production",
|
||||
"build:api": "CONNECT=false vite build --mode=production",
|
||||
"postbuild": "chmod +x dist/main.js && chmod +x dist/cli.js",
|
||||
"build:watch": "nodemon --watch src --ext ts,js,json --exec 'tsx ./scripts/build.ts'",
|
||||
"build:docker": "./scripts/dc.sh run --rm builder",
|
||||
@@ -63,7 +65,7 @@
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/core": "^11.0.11",
|
||||
"@nestjs/graphql": "^13.0.3",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/passport": "^11.0.0",
|
||||
"@nestjs/platform-fastify": "^11.0.11",
|
||||
"@nestjs/schedule": "^5.0.0",
|
||||
"@nestjs/throttler": "^6.2.1",
|
||||
@@ -99,14 +101,14 @@
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-fields": "^2.0.3",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"graphql-subscriptions": "^2.0.0",
|
||||
"graphql-subscriptions": "^3.0.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"graphql-type-uuid": "^0.2.0",
|
||||
"graphql-ws": "^5.16.0",
|
||||
"ini": "^4.1.2",
|
||||
"graphql-ws": "^6.0.0",
|
||||
"ini": "^5.0.0",
|
||||
"ip": "^2.0.1",
|
||||
"jose": "^5.9.6",
|
||||
"jose": "^6.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"multi-ini": "^2.3.2",
|
||||
"mustache": "^4.2.0",
|
||||
@@ -121,8 +123,8 @@
|
||||
"path-type": "^6.0.0",
|
||||
"pino": "^9.5.0",
|
||||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^11.3.0",
|
||||
"pm2": "^5.4.2",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"pm2": "^6.0.0",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"request": "^2.88.2",
|
||||
"rxjs": "^7.8.2",
|
||||
@@ -163,7 +165,7 @@
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/pify": "^5.0.4",
|
||||
"@types/pify": "^6.0.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/sendmail": "^1.4.7",
|
||||
"@types/stoppable": "^1.1.3",
|
||||
@@ -201,5 +203,5 @@
|
||||
}
|
||||
},
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.6.4"
|
||||
"packageManager": "pnpm@10.6.5"
|
||||
}
|
||||
|
||||
@@ -5,20 +5,25 @@ import { exit } from 'process';
|
||||
import { $, cd } from 'zx';
|
||||
|
||||
import { getDeploymentVersion } from './get-deployment-version.js';
|
||||
import { join } from 'path';
|
||||
|
||||
const workingDir = join(import.meta.dirname, '..');
|
||||
console.log('Working directory:', workingDir);
|
||||
try {
|
||||
// Create release and pack directories
|
||||
await mkdir('./deploy/release', { recursive: true });
|
||||
await mkdir('./deploy/pack', { recursive: true });
|
||||
await mkdir(join(workingDir, 'deploy/release'), { recursive: true });
|
||||
await mkdir(join(workingDir, 'deploy/pack/api'), { recursive: true });
|
||||
await mkdir(join(workingDir, 'deploy/pack/connect'), { recursive: true });
|
||||
|
||||
// Build Generated Types
|
||||
await $`pnpm run codegen`;
|
||||
|
||||
await $`pnpm run build`;
|
||||
await $`pnpm run build:api`;
|
||||
await $`pnpm run build:connect`;
|
||||
// Copy app files to plugin directory
|
||||
|
||||
// Get package details
|
||||
const packageJson = await readFile('./package.json', 'utf-8');
|
||||
const packageJson = await readFile(join(workingDir, 'package.json'), 'utf-8');
|
||||
const parsedPackageJson = JSON.parse(packageJson);
|
||||
|
||||
const deploymentVersion = await getDeploymentVersion(process.env, parsedPackageJson.version);
|
||||
@@ -26,23 +31,30 @@ try {
|
||||
// Update the package.json version to the deployment version
|
||||
parsedPackageJson.version = deploymentVersion;
|
||||
|
||||
// Create a temporary directory for packaging
|
||||
await mkdir('./deploy/pack/', { recursive: true });
|
||||
await writeFile(join(workingDir, 'deploy/pack/api/package.json'), JSON.stringify(parsedPackageJson, null, 4));
|
||||
await writeFile(join(workingDir, 'deploy/pack/connect/package.json'), JSON.stringify(parsedPackageJson, null, 4));
|
||||
|
||||
await writeFile('./deploy/pack/package.json', JSON.stringify(parsedPackageJson, null, 4));
|
||||
// Copy necessary files to the pack directory
|
||||
await $`cp -r dist README.md .env.* ecosystem.config.json ./deploy/pack/`;
|
||||
await $`cp -r dist/api/ README.md .env.* ecosystem.config.json ./deploy/pack/api/`;
|
||||
await $`cp -r dist/connect/ README.md .env.* ecosystem.config.json ./deploy/pack/connect/`;
|
||||
|
||||
// Change to the pack directory and install dependencies
|
||||
cd('./deploy/pack');
|
||||
cd(join(workingDir, 'deploy/pack/connect'));
|
||||
|
||||
console.log('Installing production dependencies...');
|
||||
$.verbose = true;
|
||||
await $`pnpm install --prod --ignore-workspace --node-linker hoisted`;
|
||||
await $`pnpm approve-builds --all`;
|
||||
|
||||
cd(join(workingDir, 'deploy/pack/api'));
|
||||
await $`cp -r ../connect/node_modules ./node_modules`;
|
||||
|
||||
// chmod the cli
|
||||
await $`chmod +x ./dist/cli.js`;
|
||||
await $`chmod +x ./dist/main.js`;
|
||||
await $`chmod +x ${join(workingDir, 'deploy/pack/api/cli.js')} \
|
||||
${join(workingDir, 'deploy/pack/api/main.js')} \
|
||||
${join(workingDir, 'deploy/pack/connect/cli.js')} \
|
||||
${join(workingDir, 'deploy/pack/connect/main.js')}`;
|
||||
|
||||
} catch (error) {
|
||||
// Error with a command
|
||||
if (Object.keys(error).includes('stderr')) {
|
||||
|
||||
@@ -949,160 +949,160 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
]
|
||||
`);
|
||||
expect(varState).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bindMgt": false,
|
||||
"cacheNumDevices": NaN,
|
||||
"cacheSbNumDisks": NaN,
|
||||
"comment": "Dev Server",
|
||||
"configState": "yes",
|
||||
"configValid": true,
|
||||
"csrfToken": "0000000000000000",
|
||||
"defaultFsType": "xfs",
|
||||
"deviceCount": 4,
|
||||
"domain": "",
|
||||
"domainLogin": "Administrator",
|
||||
"domainShort": "",
|
||||
"enableFruit": "no",
|
||||
"flashGuid": "0000-0000-0000-000000000000",
|
||||
"flashProduct": "DataTraveler_3.0",
|
||||
"flashVendor": "KINGSTON",
|
||||
"fsCopyPrcnt": 0,
|
||||
"fsNumMounted": 0,
|
||||
"fsNumUnmountable": 0,
|
||||
"fsProgress": "Autostart disabled",
|
||||
"fsState": "Stopped",
|
||||
"fsUnmountableMask": "",
|
||||
"fuseDirectio": "auto",
|
||||
"fuseDirectioDefault": "auto",
|
||||
"fuseDirectioStatus": "default",
|
||||
"fuseRemember": "330",
|
||||
"fuseRememberDefault": "330",
|
||||
"fuseRememberStatus": "default",
|
||||
"fuseUseino": "yes",
|
||||
"hideDotFiles": false,
|
||||
"joinStatus": "Not joined",
|
||||
"localMaster": true,
|
||||
"localTld": "local",
|
||||
"luksKeyfile": "/tmp/unraid/keyfile",
|
||||
"maxArraysz": 30,
|
||||
"maxCachesz": 30,
|
||||
"mdColor": "green-blink",
|
||||
"mdNumDisabled": 1,
|
||||
"mdNumDisks": 4,
|
||||
"mdNumErased": 0,
|
||||
"mdNumInvalid": 1,
|
||||
"mdNumMissing": 0,
|
||||
"mdNumNew": 0,
|
||||
"mdNumStripes": 1280,
|
||||
"mdNumStripesDefault": 1280,
|
||||
"mdNumStripesStatus": "default",
|
||||
"mdQueueLimit": "80",
|
||||
"mdQueueLimitDefault": "80",
|
||||
"mdQueueLimitStatus": "default",
|
||||
"mdResync": 0,
|
||||
"mdResyncAction": "check P",
|
||||
"mdResyncCorr": "0",
|
||||
"mdResyncDb": "0",
|
||||
"mdResyncDt": "0",
|
||||
"mdResyncPos": 0,
|
||||
"mdResyncSize": 438960096,
|
||||
"mdScheduler": "auto",
|
||||
"mdSchedulerDefault": "auto",
|
||||
"mdSchedulerStatus": "default",
|
||||
"mdState": "STOPPED",
|
||||
"mdSyncLimit": "5",
|
||||
"mdSyncLimitDefault": "5",
|
||||
"mdSyncLimitStatus": "default",
|
||||
"mdSyncThresh": NaN,
|
||||
"mdSyncThreshDefault": NaN,
|
||||
"mdSyncWindow": NaN,
|
||||
"mdSyncWindowDefault": NaN,
|
||||
"mdVersion": "2.9.14",
|
||||
"mdWriteMethod": NaN,
|
||||
"mdWriteMethodDefault": "auto",
|
||||
"mdWriteMethodStatus": "default",
|
||||
"name": "Tower",
|
||||
"nrRequests": NaN,
|
||||
"nrRequestsDefault": NaN,
|
||||
"nrRequestsStatus": "default",
|
||||
"ntpServer1": "time1.google.com",
|
||||
"ntpServer2": "time2.google.com",
|
||||
"ntpServer3": "time3.google.com",
|
||||
"ntpServer4": "time4.google.com",
|
||||
"pollAttributes": "1800",
|
||||
"pollAttributesDefault": "1800",
|
||||
"pollAttributesStatus": "default",
|
||||
"port": 80,
|
||||
"portssh": 22,
|
||||
"portssl": 443,
|
||||
"porttelnet": 23,
|
||||
"queueDepth": "auto",
|
||||
"regCheck": "Valid",
|
||||
"regExp": "",
|
||||
"regFile": "/app/dev/Unraid.net/Pro.key",
|
||||
"regGen": "0",
|
||||
"regGuid": "13FE-4200-C300-58C372A52B19",
|
||||
"regState": "PRO",
|
||||
"regTm": "1833409182",
|
||||
"regTm2": "0",
|
||||
"regTo": "Eli Bosley",
|
||||
"regTy": "PRO",
|
||||
"reservedNames": "parity,parity2,parity3,diskP,diskQ,diskR,disk,disks,flash,boot,user,user0,disk0,disk1,disk2,disk3,disk4,disk5,disk6,disk7,disk8,disk9,disk10,disk11,disk12,disk13,disk14,disk15,disk16,disk17,disk18,disk19,disk20,disk21,disk22,disk23,disk24,disk25,disk26,disk27,disk28,disk29,disk30,disk31",
|
||||
"safeMode": false,
|
||||
"sbClean": true,
|
||||
"sbEvents": 173,
|
||||
"sbName": "/boot/config/super.dat",
|
||||
"sbNumDisks": 5,
|
||||
"sbState": "1",
|
||||
"sbSyncErrs": 0,
|
||||
"sbSyncExit": "0",
|
||||
"sbSynced": 1586819259,
|
||||
"sbSynced2": 1586822456,
|
||||
"sbUpdated": "1596079143",
|
||||
"sbVersion": "2.9.13",
|
||||
"security": "user",
|
||||
"shareAvahiEnabled": true,
|
||||
"shareAvahiSmbModel": "Xserve",
|
||||
"shareAvahiSmbName": "%h",
|
||||
"shareCacheEnabled": true,
|
||||
"shareCacheFloor": "2000000",
|
||||
"shareCount": 0,
|
||||
"shareDisk": "yes",
|
||||
"shareInitialGroup": "Domain Users",
|
||||
"shareInitialOwner": "Administrator",
|
||||
"shareMoverActive": false,
|
||||
"shareMoverLogging": false,
|
||||
"shareMoverSchedule": "40 3 * * *",
|
||||
"shareNfsCount": 0,
|
||||
"shareNfsEnabled": false,
|
||||
"shareSmbCount": 1,
|
||||
"shareSmbEnabled": true,
|
||||
"shareSmbMode": "workgroup",
|
||||
"shareUser": "e",
|
||||
"shareUserExclude": "",
|
||||
"shareUserInclude": "",
|
||||
"shfsLogging": "1",
|
||||
"shutdownTimeout": 90,
|
||||
"spindownDelay": 0,
|
||||
"spinupGroups": false,
|
||||
"startArray": false,
|
||||
"startMode": "Normal",
|
||||
"startPage": "Main",
|
||||
"sysArraySlots": 24,
|
||||
"sysCacheSlots": NaN,
|
||||
"sysFlashSlots": 1,
|
||||
"sysModel": "Dell R710",
|
||||
"timeZone": "Australia/Adelaide",
|
||||
"useNetbios": "yes",
|
||||
"useNtp": true,
|
||||
"useSsh": true,
|
||||
"useSsl": null,
|
||||
"useTelnet": true,
|
||||
"useUpnp": true,
|
||||
"useWsd": "no",
|
||||
"version": "6.11.2",
|
||||
"workgroup": "WORKGROUP",
|
||||
"wsdOpt": "",
|
||||
}
|
||||
`);
|
||||
{
|
||||
"bindMgt": false,
|
||||
"cacheNumDevices": NaN,
|
||||
"cacheSbNumDisks": NaN,
|
||||
"comment": "Dev Server",
|
||||
"configErrorState": "INELIGIBLE",
|
||||
"configValid": false,
|
||||
"csrfToken": "0000000000000000",
|
||||
"defaultFsType": "xfs",
|
||||
"deviceCount": 4,
|
||||
"domain": "",
|
||||
"domainLogin": "Administrator",
|
||||
"domainShort": "",
|
||||
"enableFruit": "no",
|
||||
"flashGuid": "0000-0000-0000-000000000000",
|
||||
"flashProduct": "DataTraveler_3.0",
|
||||
"flashVendor": "KINGSTON",
|
||||
"fsCopyPrcnt": 0,
|
||||
"fsNumMounted": 0,
|
||||
"fsNumUnmountable": 0,
|
||||
"fsProgress": "Autostart disabled",
|
||||
"fsState": "Stopped",
|
||||
"fsUnmountableMask": "",
|
||||
"fuseDirectio": "auto",
|
||||
"fuseDirectioDefault": "auto",
|
||||
"fuseDirectioStatus": "default",
|
||||
"fuseRemember": "330",
|
||||
"fuseRememberDefault": "330",
|
||||
"fuseRememberStatus": "default",
|
||||
"fuseUseino": "yes",
|
||||
"hideDotFiles": false,
|
||||
"joinStatus": "Not joined",
|
||||
"localMaster": true,
|
||||
"localTld": "local",
|
||||
"luksKeyfile": "/tmp/unraid/keyfile",
|
||||
"maxArraysz": 30,
|
||||
"maxCachesz": 30,
|
||||
"mdColor": "green-blink",
|
||||
"mdNumDisabled": 1,
|
||||
"mdNumDisks": 4,
|
||||
"mdNumErased": 0,
|
||||
"mdNumInvalid": 1,
|
||||
"mdNumMissing": 0,
|
||||
"mdNumNew": 0,
|
||||
"mdNumStripes": 1280,
|
||||
"mdNumStripesDefault": 1280,
|
||||
"mdNumStripesStatus": "default",
|
||||
"mdQueueLimit": "80",
|
||||
"mdQueueLimitDefault": "80",
|
||||
"mdQueueLimitStatus": "default",
|
||||
"mdResync": 0,
|
||||
"mdResyncAction": "check P",
|
||||
"mdResyncCorr": "0",
|
||||
"mdResyncDb": "0",
|
||||
"mdResyncDt": "0",
|
||||
"mdResyncPos": 0,
|
||||
"mdResyncSize": 438960096,
|
||||
"mdScheduler": "auto",
|
||||
"mdSchedulerDefault": "auto",
|
||||
"mdSchedulerStatus": "default",
|
||||
"mdState": "STOPPED",
|
||||
"mdSyncLimit": "5",
|
||||
"mdSyncLimitDefault": "5",
|
||||
"mdSyncLimitStatus": "default",
|
||||
"mdSyncThresh": NaN,
|
||||
"mdSyncThreshDefault": NaN,
|
||||
"mdSyncWindow": NaN,
|
||||
"mdSyncWindowDefault": NaN,
|
||||
"mdVersion": "2.9.14",
|
||||
"mdWriteMethod": NaN,
|
||||
"mdWriteMethodDefault": "auto",
|
||||
"mdWriteMethodStatus": "default",
|
||||
"name": "Tower",
|
||||
"nrRequests": NaN,
|
||||
"nrRequestsDefault": NaN,
|
||||
"nrRequestsStatus": "default",
|
||||
"ntpServer1": "time1.google.com",
|
||||
"ntpServer2": "time2.google.com",
|
||||
"ntpServer3": "time3.google.com",
|
||||
"ntpServer4": "time4.google.com",
|
||||
"pollAttributes": "1800",
|
||||
"pollAttributesDefault": "1800",
|
||||
"pollAttributesStatus": "default",
|
||||
"port": 80,
|
||||
"portssh": 22,
|
||||
"portssl": 443,
|
||||
"porttelnet": 23,
|
||||
"queueDepth": "auto",
|
||||
"regCheck": "Valid",
|
||||
"regExp": "",
|
||||
"regFile": "/app/dev/Unraid.net/Pro.key",
|
||||
"regGen": "0",
|
||||
"regGuid": "13FE-4200-C300-58C372A52B19",
|
||||
"regState": "PRO",
|
||||
"regTm": "1833409182",
|
||||
"regTm2": "0",
|
||||
"regTo": "Eli Bosley",
|
||||
"regTy": "PRO",
|
||||
"reservedNames": "parity,parity2,parity3,diskP,diskQ,diskR,disk,disks,flash,boot,user,user0,disk0,disk1,disk2,disk3,disk4,disk5,disk6,disk7,disk8,disk9,disk10,disk11,disk12,disk13,disk14,disk15,disk16,disk17,disk18,disk19,disk20,disk21,disk22,disk23,disk24,disk25,disk26,disk27,disk28,disk29,disk30,disk31",
|
||||
"safeMode": false,
|
||||
"sbClean": true,
|
||||
"sbEvents": 173,
|
||||
"sbName": "/boot/config/super.dat",
|
||||
"sbNumDisks": 5,
|
||||
"sbState": "1",
|
||||
"sbSyncErrs": 0,
|
||||
"sbSyncExit": "0",
|
||||
"sbSynced": 1586819259,
|
||||
"sbSynced2": 1586822456,
|
||||
"sbUpdated": "1596079143",
|
||||
"sbVersion": "2.9.13",
|
||||
"security": "user",
|
||||
"shareAvahiEnabled": true,
|
||||
"shareAvahiSmbModel": "Xserve",
|
||||
"shareAvahiSmbName": "%h",
|
||||
"shareCacheEnabled": true,
|
||||
"shareCacheFloor": "2000000",
|
||||
"shareCount": 0,
|
||||
"shareDisk": "yes",
|
||||
"shareInitialGroup": "Domain Users",
|
||||
"shareInitialOwner": "Administrator",
|
||||
"shareMoverActive": false,
|
||||
"shareMoverLogging": false,
|
||||
"shareMoverSchedule": "40 3 * * *",
|
||||
"shareNfsCount": 0,
|
||||
"shareNfsEnabled": false,
|
||||
"shareSmbCount": 1,
|
||||
"shareSmbEnabled": true,
|
||||
"shareSmbMode": "workgroup",
|
||||
"shareUser": "e",
|
||||
"shareUserExclude": "",
|
||||
"shareUserInclude": "",
|
||||
"shfsLogging": "1",
|
||||
"shutdownTimeout": 90,
|
||||
"spindownDelay": 0,
|
||||
"spinupGroups": false,
|
||||
"startArray": false,
|
||||
"startMode": "Normal",
|
||||
"startPage": "Main",
|
||||
"sysArraySlots": 24,
|
||||
"sysCacheSlots": NaN,
|
||||
"sysFlashSlots": 1,
|
||||
"sysModel": "Dell R710",
|
||||
"timeZone": "Australia/Adelaide",
|
||||
"useNetbios": "yes",
|
||||
"useNtp": true,
|
||||
"useSsh": true,
|
||||
"useSsl": null,
|
||||
"useTelnet": true,
|
||||
"useUpnp": true,
|
||||
"useWsd": "no",
|
||||
"version": "6.11.2",
|
||||
"workgroup": "WORKGROUP",
|
||||
"wsdOpt": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -16,160 +16,160 @@ test('Returns parsed state file', async () => {
|
||||
});
|
||||
|
||||
expect(parse(stateFile)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bindMgt": false,
|
||||
"cacheNumDevices": NaN,
|
||||
"cacheSbNumDisks": NaN,
|
||||
"comment": "Dev Server",
|
||||
"configState": "yes",
|
||||
"configValid": true,
|
||||
"csrfToken": "0000000000000000",
|
||||
"defaultFsType": "xfs",
|
||||
"deviceCount": 4,
|
||||
"domain": "",
|
||||
"domainLogin": "Administrator",
|
||||
"domainShort": "",
|
||||
"enableFruit": "no",
|
||||
"flashGuid": "0000-0000-0000-000000000000",
|
||||
"flashProduct": "DataTraveler_3.0",
|
||||
"flashVendor": "KINGSTON",
|
||||
"fsCopyPrcnt": 0,
|
||||
"fsNumMounted": 0,
|
||||
"fsNumUnmountable": 0,
|
||||
"fsProgress": "Autostart disabled",
|
||||
"fsState": "Stopped",
|
||||
"fsUnmountableMask": "",
|
||||
"fuseDirectio": "auto",
|
||||
"fuseDirectioDefault": "auto",
|
||||
"fuseDirectioStatus": "default",
|
||||
"fuseRemember": "330",
|
||||
"fuseRememberDefault": "330",
|
||||
"fuseRememberStatus": "default",
|
||||
"fuseUseino": "yes",
|
||||
"hideDotFiles": false,
|
||||
"joinStatus": "Not joined",
|
||||
"localMaster": true,
|
||||
"localTld": "local",
|
||||
"luksKeyfile": "/tmp/unraid/keyfile",
|
||||
"maxArraysz": 30,
|
||||
"maxCachesz": 30,
|
||||
"mdColor": "green-blink",
|
||||
"mdNumDisabled": 1,
|
||||
"mdNumDisks": 4,
|
||||
"mdNumErased": 0,
|
||||
"mdNumInvalid": 1,
|
||||
"mdNumMissing": 0,
|
||||
"mdNumNew": 0,
|
||||
"mdNumStripes": 1280,
|
||||
"mdNumStripesDefault": 1280,
|
||||
"mdNumStripesStatus": "default",
|
||||
"mdQueueLimit": "80",
|
||||
"mdQueueLimitDefault": "80",
|
||||
"mdQueueLimitStatus": "default",
|
||||
"mdResync": 0,
|
||||
"mdResyncAction": "check P",
|
||||
"mdResyncCorr": "0",
|
||||
"mdResyncDb": "0",
|
||||
"mdResyncDt": "0",
|
||||
"mdResyncPos": 0,
|
||||
"mdResyncSize": 438960096,
|
||||
"mdScheduler": "auto",
|
||||
"mdSchedulerDefault": "auto",
|
||||
"mdSchedulerStatus": "default",
|
||||
"mdState": "STOPPED",
|
||||
"mdSyncLimit": "5",
|
||||
"mdSyncLimitDefault": "5",
|
||||
"mdSyncLimitStatus": "default",
|
||||
"mdSyncThresh": NaN,
|
||||
"mdSyncThreshDefault": NaN,
|
||||
"mdSyncWindow": NaN,
|
||||
"mdSyncWindowDefault": NaN,
|
||||
"mdVersion": "2.9.14",
|
||||
"mdWriteMethod": NaN,
|
||||
"mdWriteMethodDefault": "auto",
|
||||
"mdWriteMethodStatus": "default",
|
||||
"name": "Tower",
|
||||
"nrRequests": NaN,
|
||||
"nrRequestsDefault": NaN,
|
||||
"nrRequestsStatus": "default",
|
||||
"ntpServer1": "time1.google.com",
|
||||
"ntpServer2": "time2.google.com",
|
||||
"ntpServer3": "time3.google.com",
|
||||
"ntpServer4": "time4.google.com",
|
||||
"pollAttributes": "1800",
|
||||
"pollAttributesDefault": "1800",
|
||||
"pollAttributesStatus": "default",
|
||||
"port": 80,
|
||||
"portssh": 22,
|
||||
"portssl": 443,
|
||||
"porttelnet": 23,
|
||||
"queueDepth": "auto",
|
||||
"regCheck": "Valid",
|
||||
"regExp": "",
|
||||
"regFile": "/app/dev/Unraid.net/Pro.key",
|
||||
"regGen": "0",
|
||||
"regGuid": "13FE-4200-C300-58C372A52B19",
|
||||
"regState": "PRO",
|
||||
"regTm": "1833409182",
|
||||
"regTm2": "0",
|
||||
"regTo": "Eli Bosley",
|
||||
"regTy": "PRO",
|
||||
"reservedNames": "parity,parity2,parity3,diskP,diskQ,diskR,disk,disks,flash,boot,user,user0,disk0,disk1,disk2,disk3,disk4,disk5,disk6,disk7,disk8,disk9,disk10,disk11,disk12,disk13,disk14,disk15,disk16,disk17,disk18,disk19,disk20,disk21,disk22,disk23,disk24,disk25,disk26,disk27,disk28,disk29,disk30,disk31",
|
||||
"safeMode": false,
|
||||
"sbClean": true,
|
||||
"sbEvents": 173,
|
||||
"sbName": "/boot/config/super.dat",
|
||||
"sbNumDisks": 5,
|
||||
"sbState": "1",
|
||||
"sbSyncErrs": 0,
|
||||
"sbSyncExit": "0",
|
||||
"sbSynced": 1586819259,
|
||||
"sbSynced2": 1586822456,
|
||||
"sbUpdated": "1596079143",
|
||||
"sbVersion": "2.9.13",
|
||||
"security": "user",
|
||||
"shareAvahiEnabled": true,
|
||||
"shareAvahiSmbModel": "Xserve",
|
||||
"shareAvahiSmbName": "%h",
|
||||
"shareCacheEnabled": true,
|
||||
"shareCacheFloor": "2000000",
|
||||
"shareCount": 0,
|
||||
"shareDisk": "yes",
|
||||
"shareInitialGroup": "Domain Users",
|
||||
"shareInitialOwner": "Administrator",
|
||||
"shareMoverActive": false,
|
||||
"shareMoverLogging": false,
|
||||
"shareMoverSchedule": "40 3 * * *",
|
||||
"shareNfsCount": 0,
|
||||
"shareNfsEnabled": false,
|
||||
"shareSmbCount": 1,
|
||||
"shareSmbEnabled": true,
|
||||
"shareSmbMode": "workgroup",
|
||||
"shareUser": "e",
|
||||
"shareUserExclude": "",
|
||||
"shareUserInclude": "",
|
||||
"shfsLogging": "1",
|
||||
"shutdownTimeout": 90,
|
||||
"spindownDelay": 0,
|
||||
"spinupGroups": false,
|
||||
"startArray": false,
|
||||
"startMode": "Normal",
|
||||
"startPage": "Main",
|
||||
"sysArraySlots": 24,
|
||||
"sysCacheSlots": NaN,
|
||||
"sysFlashSlots": 1,
|
||||
"sysModel": "Dell R710",
|
||||
"timeZone": "Australia/Adelaide",
|
||||
"useNetbios": "yes",
|
||||
"useNtp": true,
|
||||
"useSsh": true,
|
||||
"useSsl": null,
|
||||
"useTelnet": true,
|
||||
"useUpnp": true,
|
||||
"useWsd": "no",
|
||||
"version": "6.11.2",
|
||||
"workgroup": "WORKGROUP",
|
||||
"wsdOpt": "",
|
||||
}
|
||||
`);
|
||||
{
|
||||
"bindMgt": false,
|
||||
"cacheNumDevices": NaN,
|
||||
"cacheSbNumDisks": NaN,
|
||||
"comment": "Dev Server",
|
||||
"configErrorState": "INELIGIBLE",
|
||||
"configValid": false,
|
||||
"csrfToken": "0000000000000000",
|
||||
"defaultFsType": "xfs",
|
||||
"deviceCount": 4,
|
||||
"domain": "",
|
||||
"domainLogin": "Administrator",
|
||||
"domainShort": "",
|
||||
"enableFruit": "no",
|
||||
"flashGuid": "0000-0000-0000-000000000000",
|
||||
"flashProduct": "DataTraveler_3.0",
|
||||
"flashVendor": "KINGSTON",
|
||||
"fsCopyPrcnt": 0,
|
||||
"fsNumMounted": 0,
|
||||
"fsNumUnmountable": 0,
|
||||
"fsProgress": "Autostart disabled",
|
||||
"fsState": "Stopped",
|
||||
"fsUnmountableMask": "",
|
||||
"fuseDirectio": "auto",
|
||||
"fuseDirectioDefault": "auto",
|
||||
"fuseDirectioStatus": "default",
|
||||
"fuseRemember": "330",
|
||||
"fuseRememberDefault": "330",
|
||||
"fuseRememberStatus": "default",
|
||||
"fuseUseino": "yes",
|
||||
"hideDotFiles": false,
|
||||
"joinStatus": "Not joined",
|
||||
"localMaster": true,
|
||||
"localTld": "local",
|
||||
"luksKeyfile": "/tmp/unraid/keyfile",
|
||||
"maxArraysz": 30,
|
||||
"maxCachesz": 30,
|
||||
"mdColor": "green-blink",
|
||||
"mdNumDisabled": 1,
|
||||
"mdNumDisks": 4,
|
||||
"mdNumErased": 0,
|
||||
"mdNumInvalid": 1,
|
||||
"mdNumMissing": 0,
|
||||
"mdNumNew": 0,
|
||||
"mdNumStripes": 1280,
|
||||
"mdNumStripesDefault": 1280,
|
||||
"mdNumStripesStatus": "default",
|
||||
"mdQueueLimit": "80",
|
||||
"mdQueueLimitDefault": "80",
|
||||
"mdQueueLimitStatus": "default",
|
||||
"mdResync": 0,
|
||||
"mdResyncAction": "check P",
|
||||
"mdResyncCorr": "0",
|
||||
"mdResyncDb": "0",
|
||||
"mdResyncDt": "0",
|
||||
"mdResyncPos": 0,
|
||||
"mdResyncSize": 438960096,
|
||||
"mdScheduler": "auto",
|
||||
"mdSchedulerDefault": "auto",
|
||||
"mdSchedulerStatus": "default",
|
||||
"mdState": "STOPPED",
|
||||
"mdSyncLimit": "5",
|
||||
"mdSyncLimitDefault": "5",
|
||||
"mdSyncLimitStatus": "default",
|
||||
"mdSyncThresh": NaN,
|
||||
"mdSyncThreshDefault": NaN,
|
||||
"mdSyncWindow": NaN,
|
||||
"mdSyncWindowDefault": NaN,
|
||||
"mdVersion": "2.9.14",
|
||||
"mdWriteMethod": NaN,
|
||||
"mdWriteMethodDefault": "auto",
|
||||
"mdWriteMethodStatus": "default",
|
||||
"name": "Tower",
|
||||
"nrRequests": NaN,
|
||||
"nrRequestsDefault": NaN,
|
||||
"nrRequestsStatus": "default",
|
||||
"ntpServer1": "time1.google.com",
|
||||
"ntpServer2": "time2.google.com",
|
||||
"ntpServer3": "time3.google.com",
|
||||
"ntpServer4": "time4.google.com",
|
||||
"pollAttributes": "1800",
|
||||
"pollAttributesDefault": "1800",
|
||||
"pollAttributesStatus": "default",
|
||||
"port": 80,
|
||||
"portssh": 22,
|
||||
"portssl": 443,
|
||||
"porttelnet": 23,
|
||||
"queueDepth": "auto",
|
||||
"regCheck": "Valid",
|
||||
"regExp": "",
|
||||
"regFile": "/app/dev/Unraid.net/Pro.key",
|
||||
"regGen": "0",
|
||||
"regGuid": "13FE-4200-C300-58C372A52B19",
|
||||
"regState": "PRO",
|
||||
"regTm": "1833409182",
|
||||
"regTm2": "0",
|
||||
"regTo": "Eli Bosley",
|
||||
"regTy": "PRO",
|
||||
"reservedNames": "parity,parity2,parity3,diskP,diskQ,diskR,disk,disks,flash,boot,user,user0,disk0,disk1,disk2,disk3,disk4,disk5,disk6,disk7,disk8,disk9,disk10,disk11,disk12,disk13,disk14,disk15,disk16,disk17,disk18,disk19,disk20,disk21,disk22,disk23,disk24,disk25,disk26,disk27,disk28,disk29,disk30,disk31",
|
||||
"safeMode": false,
|
||||
"sbClean": true,
|
||||
"sbEvents": 173,
|
||||
"sbName": "/boot/config/super.dat",
|
||||
"sbNumDisks": 5,
|
||||
"sbState": "1",
|
||||
"sbSyncErrs": 0,
|
||||
"sbSyncExit": "0",
|
||||
"sbSynced": 1586819259,
|
||||
"sbSynced2": 1586822456,
|
||||
"sbUpdated": "1596079143",
|
||||
"sbVersion": "2.9.13",
|
||||
"security": "user",
|
||||
"shareAvahiEnabled": true,
|
||||
"shareAvahiSmbModel": "Xserve",
|
||||
"shareAvahiSmbName": "%h",
|
||||
"shareCacheEnabled": true,
|
||||
"shareCacheFloor": "2000000",
|
||||
"shareCount": 0,
|
||||
"shareDisk": "yes",
|
||||
"shareInitialGroup": "Domain Users",
|
||||
"shareInitialOwner": "Administrator",
|
||||
"shareMoverActive": false,
|
||||
"shareMoverLogging": false,
|
||||
"shareMoverSchedule": "40 3 * * *",
|
||||
"shareNfsCount": 0,
|
||||
"shareNfsEnabled": false,
|
||||
"shareSmbCount": 1,
|
||||
"shareSmbEnabled": true,
|
||||
"shareSmbMode": "workgroup",
|
||||
"shareUser": "e",
|
||||
"shareUserExclude": "",
|
||||
"shareUserInclude": "",
|
||||
"shfsLogging": "1",
|
||||
"shutdownTimeout": 90,
|
||||
"spindownDelay": 0,
|
||||
"spinupGroups": false,
|
||||
"startArray": false,
|
||||
"startMode": "Normal",
|
||||
"startPage": "Main",
|
||||
"sysArraySlots": 24,
|
||||
"sysCacheSlots": NaN,
|
||||
"sysFlashSlots": 1,
|
||||
"sysModel": "Dell R710",
|
||||
"timeZone": "Australia/Adelaide",
|
||||
"useNetbios": "yes",
|
||||
"useNtp": true,
|
||||
"useSsh": true,
|
||||
"useSsl": null,
|
||||
"useTelnet": true,
|
||||
"useUpnp": true,
|
||||
"useWsd": "no",
|
||||
"version": "6.11.2",
|
||||
"workgroup": "WORKGROUP",
|
||||
"wsdOpt": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -28,5 +28,5 @@ export const pubsub = new PubSub({ eventEmitter });
|
||||
* @param channel The pubsub channel to subscribe to.
|
||||
*/
|
||||
export const createSubscription = (channel: PUBSUB_CHANNEL) => {
|
||||
return pubsub.asyncIterator(channel);
|
||||
return pubsub.asyncIterableIterator(channel);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {
|
||||
type ArrayState,
|
||||
type DiskFsType,
|
||||
type RegistrationState,
|
||||
type registrationType,
|
||||
import type {
|
||||
ArrayState,
|
||||
DiskFsType,
|
||||
RegistrationState,
|
||||
registrationType,
|
||||
} from '@app/graphql/generated/api/types.js';
|
||||
import { ConfigErrorState } from '@app/graphql/generated/api/types.js';
|
||||
|
||||
/**
|
||||
* Global vars
|
||||
@@ -17,7 +18,7 @@ export type Var = {
|
||||
/** Is the array's config valid. */
|
||||
configValid: boolean;
|
||||
/** @internal used to hold the value for config.error */
|
||||
configState: string;
|
||||
configErrorState: ConfigErrorState | null;
|
||||
/** Current CSRF token for HTTP requests with emhttpd. */
|
||||
csrfToken: string;
|
||||
defaultFormat: string;
|
||||
|
||||
@@ -65,3 +65,9 @@ export const MOTHERSHIP_GRAPHQL_LINK = process.env.MOTHERSHIP_GRAPHQL_LINK
|
||||
: 'https://mothership.unraid.net/ws';
|
||||
|
||||
export const PM2_HOME = process.env.PM2_HOME ?? join(homedir(), '.pm2');
|
||||
/**
|
||||
* Whether the API is running in the context of Unraid Connect
|
||||
*
|
||||
* When true, enables Connect specific features
|
||||
*/
|
||||
export const CONNECT = process.env.CONNECT;
|
||||
|
||||
@@ -611,6 +611,7 @@ export function LogFileContentSchema(): z.ZodObject<Properties<LogFileContent>>
|
||||
__typename: z.literal('LogFileContent').optional(),
|
||||
content: z.string(),
|
||||
path: z.string(),
|
||||
startLine: z.number().nullish(),
|
||||
totalLines: z.number()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -665,6 +665,8 @@ export type LogFileContent = {
|
||||
content: Scalars['String']['output'];
|
||||
/** Path to the log file */
|
||||
path: Scalars['String']['output'];
|
||||
/** Starting line number of the content (1-indexed) */
|
||||
startLine?: Maybe<Scalars['Int']['output']>;
|
||||
/** Total number of lines in the file */
|
||||
totalLines: Scalars['Int']['output'];
|
||||
};
|
||||
@@ -1156,6 +1158,7 @@ export type Query = {
|
||||
* Get the content of a specific log file
|
||||
* @param path Path to the log file
|
||||
* @param lines Number of lines to read from the end of the file (default: 100)
|
||||
* @param startLine Optional starting line number (1-indexed)
|
||||
*/
|
||||
logFile: LogFileContent;
|
||||
/** List all available log files */
|
||||
@@ -1213,6 +1216,7 @@ export type QuerydockerNetworksArgs = {
|
||||
export type QuerylogFileArgs = {
|
||||
lines?: InputMaybe<Scalars['Int']['input']>;
|
||||
path: Scalars['String']['input'];
|
||||
startLine?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -2559,6 +2563,7 @@ export type LogFileResolvers<ContextType = Context, ParentType extends Resolvers
|
||||
export type LogFileContentResolvers<ContextType = Context, ParentType extends ResolversParentTypes['LogFileContent'] = ResolversParentTypes['LogFileContent']> = ResolversObject<{
|
||||
content?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
path?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
startLine?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
|
||||
totalLines?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
@@ -13,12 +13,7 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
|
||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
|
||||
*/
|
||||
type Documents = {
|
||||
"\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": typeof types.sendRemoteGraphQLResponseDocument,
|
||||
"\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": typeof types.RemoteGraphQLEventFragmentFragmentDoc,
|
||||
"\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": typeof types.eventsDocument,
|
||||
};
|
||||
const documents: Documents = {
|
||||
const documents = {
|
||||
"\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": types.sendRemoteGraphQLResponseDocument,
|
||||
"\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": types.RemoteGraphQLEventFragmentFragmentDoc,
|
||||
"\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": types.eventsDocument,
|
||||
|
||||
@@ -68,7 +68,7 @@ export const createSubscription = (channel: string, resource?: string) => ({
|
||||
});
|
||||
|
||||
hasSubscribedToChannel(context.websocketId, channel);
|
||||
return pubsub.asyncIterator(channel);
|
||||
return pubsub.asyncIterableIterator(channel);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { minigraphLogger, mothershipLogger } from '@app/core/log.js';
|
||||
import { CONNECT } from '@app/environment.js';
|
||||
import { useFragment } from '@app/graphql/generated/client/fragment-masking.js';
|
||||
import { ClientType } from '@app/graphql/generated/client/graphql.js';
|
||||
import { EVENTS_SUBSCRIPTION, RemoteGraphQL_Fragment } from '@app/graphql/mothership/subscriptions.js';
|
||||
@@ -75,14 +76,18 @@ export const subscribeToEvents = async (apiKey: string) => {
|
||||
};
|
||||
|
||||
export const setupNewMothershipSubscription = async (state = store.getState()) => {
|
||||
await GraphQLClient.clearInstance();
|
||||
if (getMothershipConnectionParams(state)?.apiKey) {
|
||||
minigraphLogger.trace('Creating Graphql client');
|
||||
const client = GraphQLClient.createSingletonInstance();
|
||||
if (client) {
|
||||
minigraphLogger.trace('Connecting to mothership');
|
||||
await subscribeToEvents(state.config.remote.apikey);
|
||||
initPingTimeoutJobs();
|
||||
if (CONNECT) {
|
||||
await GraphQLClient.clearInstance();
|
||||
if (getMothershipConnectionParams(state)?.apiKey) {
|
||||
minigraphLogger.trace('Creating Graphql client');
|
||||
const client = GraphQLClient.createSingletonInstance();
|
||||
if (client) {
|
||||
minigraphLogger.trace('Connecting to mothership');
|
||||
await subscribeToEvents(state.config.remote.apikey);
|
||||
initPingTimeoutJobs();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
minigraphLogger.trace('In API mode, CONNECT is false - skipping mothership subscription');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'reflect-metadata';
|
||||
import type { TypedAddListener, TypedStartListening } from '@reduxjs/toolkit';
|
||||
import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
|
||||
|
||||
import { CONNECT } from '@app/environment.js';
|
||||
import { type AppDispatch, type RootState } from '@app/store/index.js';
|
||||
import { enableArrayEventListener } from '@app/store/listeners/array-event-listener.js';
|
||||
import { enableConfigFileListener } from '@app/store/listeners/config-listener.js';
|
||||
@@ -25,13 +26,17 @@ export const addAppListener = addListener as TypedAddListener<RootState, AppDisp
|
||||
|
||||
export const startMiddlewareListeners = () => {
|
||||
// Begin listening for events
|
||||
enableMothershipJobsListener();
|
||||
if (CONNECT) {
|
||||
enableMothershipJobsListener();
|
||||
}
|
||||
enableConfigFileListener('flash')();
|
||||
enableConfigFileListener('memory')();
|
||||
enableUpnpListener();
|
||||
enableVersionListener();
|
||||
enableDynamicRemoteAccessListener();
|
||||
enableArrayEventListener();
|
||||
enableWanAccessChangeListener();
|
||||
enableServerStateListener();
|
||||
if (CONNECT) {
|
||||
enableUpnpListener();
|
||||
enableDynamicRemoteAccessListener();
|
||||
enableWanAccessChangeListener();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type IniStringBoolean, type IniStringBooleanOrAuto } from '@app/core/ty
|
||||
import { toNumber } from '@app/core/utils/index.js';
|
||||
import {
|
||||
ArrayState,
|
||||
ConfigErrorState,
|
||||
DiskFsType,
|
||||
RegistrationState,
|
||||
registrationType,
|
||||
@@ -23,7 +24,7 @@ export type VarIni = {
|
||||
cacheSbNumDisks: string;
|
||||
comment: string;
|
||||
configValid: string;
|
||||
configState: string;
|
||||
configErrorState: string;
|
||||
csrfToken: string;
|
||||
defaultFormat: string;
|
||||
defaultFsType: string;
|
||||
@@ -200,6 +201,10 @@ const safeParseMdState = (mdState: string | undefined): ArrayState => {
|
||||
return attemptedParse;
|
||||
};
|
||||
|
||||
export const convertconfigErrorStateToEnum = (configErrorState: string): ConfigErrorState => {
|
||||
return ConfigErrorState[configErrorState.toUpperCase()];
|
||||
};
|
||||
|
||||
export const parse: StateFileToIniParserMap['var'] = (iniFile) => {
|
||||
return {
|
||||
...iniFile,
|
||||
@@ -209,7 +214,9 @@ export const parse: StateFileToIniParserMap['var'] = (iniFile) => {
|
||||
cacheNumDevices: toNumber(iniFile.cacheNumDevices),
|
||||
cacheSbNumDisks: toNumber(iniFile.cacheSbNumDisks),
|
||||
configValid: iniBooleanToJsBoolean(iniFile.configValid, false),
|
||||
configState: iniFile.configValid,
|
||||
configErrorState: iniBooleanToJsBoolean(iniFile.configValid, false)
|
||||
? null
|
||||
: convertconfigErrorStateToEnum(iniFile.configValid),
|
||||
deviceCount: toNumber(iniFile.deviceCount),
|
||||
fsCopyPrcnt: toNumber(iniFile.fsCopyPrcnt),
|
||||
fsNumMounted: toNumber(iniFile.fsNumMounted),
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
} from '@app/graphql/generated/api/types.js';
|
||||
import type { DataSlice, SettingSlice, UIElement } from '@app/unraid-api/types/json-forms.js';
|
||||
import { fileExistsSync } from '@app/core/utils/files/file-exists.js';
|
||||
import { CONNECT } from '@app/environment.js';
|
||||
import {
|
||||
DynamicRemoteAccessType,
|
||||
WAN_ACCESS_TYPE,
|
||||
@@ -147,16 +148,16 @@ export class ConnectSettingsService {
|
||||
* Builds the complete settings schema
|
||||
*/
|
||||
async buildSettingsSchema(): Promise<SettingSlice> {
|
||||
const slices = [
|
||||
await this.remoteAccessSlice(),
|
||||
await this.sandboxSlice(),
|
||||
this.flashBackupSlice(),
|
||||
// Because CORS is effectively disabled, this setting is no longer necessary
|
||||
// keeping it here for in case it needs to be re-enabled
|
||||
//
|
||||
// this.extraOriginsSlice(),
|
||||
];
|
||||
|
||||
const slices: SettingSlice[] = [];
|
||||
if (CONNECT) {
|
||||
slices.push(await this.remoteAccessSlice());
|
||||
slices.push(this.flashBackupSlice());
|
||||
}
|
||||
slices.push(await this.sandboxSlice());
|
||||
// Because CORS is effectively disabled, this setting is no longer necessary
|
||||
// keeping it here for in case it needs to be re-enabled
|
||||
//
|
||||
// this.extraOriginsSlice(),
|
||||
return mergeSettingSlices(slices);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,7 @@ export class ConfigResolver {
|
||||
return {
|
||||
id: 'config',
|
||||
valid: emhttp.var.configValid,
|
||||
error: emhttp.var.configValid
|
||||
? null
|
||||
: (ConfigErrorState[emhttp.var.configState] ?? ConfigErrorState.UNKNOWN_ERROR),
|
||||
error: emhttp.var.configValid ? null : emhttp.var.configErrorState,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1741960696861
|
||||
1742838682466
|
||||
@@ -1 +1 @@
|
||||
1741960696132
|
||||
1742838681735
|
||||
@@ -1 +1 @@
|
||||
1741960696408
|
||||
1742838682135
|
||||
@@ -1 +1 @@
|
||||
1741960697108
|
||||
1742838682936
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { readFile, rm, writeFile } from 'node:fs/promises';
|
||||
|
||||
import { fileExists } from '@app/core/utils/files/file-exists.js';
|
||||
import {
|
||||
FileModification,
|
||||
ShouldApplyWithReason,
|
||||
} from '@app/unraid-api/unraid-file-modifier/file-modification.js';
|
||||
|
||||
export class LogViewerModification extends FileModification {
|
||||
id: string = 'log-viewer';
|
||||
public readonly filePath: string =
|
||||
'/usr/local/emhttp/plugins/dynamix.my.servers/LogViewer.page' as const;
|
||||
|
||||
private readonly logViewerConfig: string = `
|
||||
Menu="UNRAID-OS"
|
||||
Title="Log Viewer (new)"
|
||||
Icon="icon-log"
|
||||
Tag="list"
|
||||
---
|
||||
<unraid-i18n-host>
|
||||
<unraid-log-viewer></unraid-log-viewer>
|
||||
</unraid-i18n-host>
|
||||
|
||||
`.trimStart();
|
||||
|
||||
constructor(logger: Logger) {
|
||||
super(logger);
|
||||
}
|
||||
|
||||
protected async generatePatch(overridePath?: string): Promise<string> {
|
||||
const currentContent = (await fileExists(this.filePath))
|
||||
? await readFile(this.filePath, 'utf8')
|
||||
: '';
|
||||
|
||||
return this.createPatchWithDiff(
|
||||
overridePath ?? this.filePath,
|
||||
currentContent,
|
||||
this.logViewerConfig
|
||||
);
|
||||
}
|
||||
|
||||
async shouldApply(): Promise<ShouldApplyWithReason> {
|
||||
const alreadyConfigured = await fileExists(this.filePath);
|
||||
if (alreadyConfigured) {
|
||||
return { shouldApply: false, reason: 'LogViewer configuration already exists' };
|
||||
}
|
||||
return { shouldApply: true, reason: 'No LogViewer config for the API configured yet' };
|
||||
}
|
||||
|
||||
async apply(): Promise<string> {
|
||||
await this.rollback();
|
||||
await writeFile(this.filePath, this.logViewerConfig, { mode: 0o644 });
|
||||
return this.logViewerConfig;
|
||||
}
|
||||
|
||||
async rollback(): Promise<void> {
|
||||
await rm(this.getPathToAppliedPatch(), { force: true });
|
||||
await rm(this.filePath, { force: true });
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { FileModification } from '@app/unraid-api/unraid-file-modifier/file-modi
|
||||
import AuthRequestModification from '@app/unraid-api/unraid-file-modifier/modifications/auth-request.modification.js';
|
||||
import DefaultPageLayoutModification from '@app/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.js';
|
||||
import { LogRotateModification } from '@app/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.js';
|
||||
import { LogViewerModification } from '@app/unraid-api/unraid-file-modifier/modifications/log-viewer.modification.js';
|
||||
import NotificationsPageModification from '@app/unraid-api/unraid-file-modifier/modifications/notifications-page.modification.js';
|
||||
import SSOFileModification from '@app/unraid-api/unraid-file-modifier/modifications/sso.modification.js';
|
||||
|
||||
@@ -42,6 +43,7 @@ export class UnraidFileModificationService implements OnModuleInit, OnModuleDest
|
||||
async loadModifications(): Promise<FileModification[]> {
|
||||
const modifications: FileModification[] = [];
|
||||
const modificationClasses: Array<new (logger: Logger) => FileModification> = [
|
||||
LogViewerModification,
|
||||
LogRotateModification,
|
||||
AuthRequestModification,
|
||||
SSOFileModification,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { VitePluginNode } from 'vite-plugin-node';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const isConnect = process.env.CONNECT === 'true';
|
||||
export default defineConfig(({ mode }): ViteUserConfig => {
|
||||
return {
|
||||
assetsInclude: ['src/**/*.graphql', 'src/**/*.patch'],
|
||||
@@ -58,6 +59,7 @@ export default defineConfig(({ mode }): ViteUserConfig => {
|
||||
define: {
|
||||
// Allows vite to preserve process.env variables and not hardcode them
|
||||
'process.env': 'process.env',
|
||||
'process.env.CONNECT': isConnect,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
@@ -80,7 +82,7 @@ export default defineConfig(({ mode }): ViteUserConfig => {
|
||||
build: {
|
||||
ssr: true,
|
||||
sourcemap: false,
|
||||
outDir: 'dist',
|
||||
outDir: isConnect ? 'dist/connect/' : 'dist/api/',
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: 'src/index.ts',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "unraid-monorepo",
|
||||
"private": true,
|
||||
"version": "4.2.1",
|
||||
"version": "4.4.0",
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
"build:watch": "pnpm -r build:watch",
|
||||
@@ -35,5 +35,5 @@
|
||||
"dependencies": {
|
||||
"@manypkg/cli": "^0.23.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.4"
|
||||
"packageManager": "pnpm@10.6.5"
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ Then install the plugin on your Unraid development machine by visiting:
|
||||
|
||||
Then paste the following URL into the Unraid Plugins page:
|
||||
|
||||
`http://YOUR_LOCAL_DEV_MACHINE_IP:8080/plugins/local/dynamix.unraid.net.plg`
|
||||
`http://YOUR_LOCAL_DEV_MACHINE_IP:5858/plugins/local/dynamix.unraid.net.plg`
|
||||
|
||||
Replace `SERVER_NAME` with your development machine's hostname.
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export const setupPluginEnv = async (argv: string[]): Promise<PluginEnv> => {
|
||||
"Base URL - will be used to determine the bucket, and combined with the tag (if set) to form the final URL",
|
||||
process.env.CI === "true"
|
||||
? "This is a CI build, please set the base URL manually"
|
||||
: `http://${process.env.HOST_LAN_IP}:8080`
|
||||
: `http://${process.env.HOST_LAN_IP}:5858`
|
||||
)
|
||||
.option(
|
||||
"--txz-path <path>",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/connect-plugin",
|
||||
"version": "4.2.1",
|
||||
"version": "4.4.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"commander": "^13.1.0",
|
||||
@@ -21,7 +21,7 @@
|
||||
"build:txz": "tsx builder/build-txz.ts",
|
||||
"build:plugin": "tsx builder/build-plugin.ts",
|
||||
"build:validate": "npm run env:validate && npm run build",
|
||||
"build:watcher": "nodemon --verbose --watch 'source/**/*' --ext ts,js --ignore '*.test.ts' --ignore 'node_modules/**' --ignore 'source/dynamix.unraid.net/install/**' --delay 5s --exec 'pnpm run build'",
|
||||
"build:watcher": "nodemon --verbose --watch 'source/**/*' --watch 'plugins/dynamix.unraid.net.plg' --ext ts,js,plg --ignore '*.test.ts' --ignore 'node_modules/**' --ignore 'source/dynamix.unraid.net/install/**' --delay 5s --exec 'pnpm run build'",
|
||||
"// Docker commands": "",
|
||||
"build:watch": "./scripts/dc.sh pnpm run build:watcher",
|
||||
"docker:build": "docker compose build",
|
||||
@@ -39,5 +39,5 @@
|
||||
"nodemon": "^3.1.7",
|
||||
"vitest": "^3.0.7"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.4"
|
||||
"packageManager": "pnpm@10.6.5"
|
||||
}
|
||||
|
||||
@@ -88,8 +88,6 @@ DNSERR=no
|
||||
|
||||
echo "Checking DNS..."
|
||||
dnscheck "mothership.unraid.net"
|
||||
#dnscheck "wanip4.unraid.net"
|
||||
#dnscheck "backup.unraid.net"
|
||||
|
||||
[[ "${DNSERR}" == "yes" && "${DNS_SERVER1}" != "8.8.8.8" ]] && echo " Recommend navigating to Settings -> Network Settings and changing your DNS server to 8.8.8.8"
|
||||
# Note: DNS checks will fail if the network is not available at boot. Cannot exit the install when DNS checks fail.
|
||||
@@ -134,7 +132,7 @@ exit 0
|
||||
fi
|
||||
|
||||
# Remove all node js archives from the flashdrive that do not match the expected version
|
||||
find /boot/config/plugins/dynamix.my.servers/ -name "node-v*-linux-x64.tar.xz" ! -name "&NODEJS_FILENAME;" -delete
|
||||
find /boot/config/plugins/dynamix.my.servers/ -name "node-v*-linux-x64.tar.xz" ! -name "${NODE_FILE}" -delete
|
||||
|
||||
|
||||
echo "Node.js installation successful"
|
||||
@@ -150,18 +148,6 @@ exit 0
|
||||
<SHA256>&TXZ_SHA256;</SHA256>
|
||||
</FILE>
|
||||
|
||||
<FILE Run="/bin/bash" Method="install">
|
||||
<INLINE>
|
||||
MAINTXZ="&source;.txz"
|
||||
<![CDATA[
|
||||
# before proceeding with install, doubly confirm downloaded files exist. just being pedantic.
|
||||
FILE=${MAINTXZ} && [[ ! -f "$FILE" ]] && echo "⚠️ file missing - $FILE" && exit 1
|
||||
|
||||
exit 0
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<FILE Run="/bin/bash" Method="remove">
|
||||
<INLINE>
|
||||
<![CDATA[
|
||||
@@ -172,6 +158,10 @@ source /etc/unraid-version
|
||||
# Undo some activation / partner setup
|
||||
source /usr/local/emhttp/plugins/dynamix.my.servers/scripts/activation_code_remove
|
||||
|
||||
# Run cleanup operations
|
||||
echo "Performing cleanup operations..."
|
||||
/usr/bin/php /usr/local/emhttp/plugins/dynamix.my.servers/scripts/cleanup_operations.php
|
||||
|
||||
echo
|
||||
echo "⚠️ Do not close this window yet"
|
||||
echo
|
||||
@@ -181,144 +171,28 @@ exit 0
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<!-- disable features on uninstall -->
|
||||
<!-- NOTE: this script is PHP not bash -->
|
||||
<FILE Run="/usr/bin/php" Method="remove">
|
||||
<INLINE>
|
||||
<![CDATA[
|
||||
<?
|
||||
$msini = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true);
|
||||
|
||||
echo "\n";
|
||||
echo "**********************************\n";
|
||||
echo "🧹 CLEANING UP - may take a minute\n";
|
||||
echo "**********************************\n";
|
||||
|
||||
if (file_exists("/boot/.git")) {
|
||||
if (file_exists("/etc/rc.d/rc.flash_backup")) {
|
||||
# stop flash backup service
|
||||
echo "\nStopping flash backup service. Please wait…";
|
||||
exec("/etc/rc.d/rc.flash_backup stop &>/dev/null");
|
||||
}
|
||||
if (file_exists("/usr/local/emhttp/plugins/dynamix.my.servers/include/UpdateFlashBackup.php")) {
|
||||
# deactivate and delete local flash backup
|
||||
echo "\nDeactivating flash backup. Please wait…";
|
||||
passthru("/usr/bin/php /usr/local/emhttp/plugins/dynamix.my.servers/include/UpdateFlashBackup.php deactivate");
|
||||
}
|
||||
}
|
||||
|
||||
if (file_exists("/etc/rc.d/rc.unraid-api")) {
|
||||
echo "\nStopping unraid-api. Please wait…";
|
||||
$output = shell_exec("/etc/rc.d/rc.unraid-api stop --delete 2>&1'");
|
||||
if (!$output) {
|
||||
echo "Waiting for unraid-api to stop...\n";
|
||||
sleep(5); // Give it a few seconds to fully stop
|
||||
}
|
||||
echo "Stopped unraid-api: $output";
|
||||
|
||||
# Find all PIDs referencing main.js and kill them, excluding grep process
|
||||
$pids = shell_exec("ps aux | grep 'node /usr/local/unraid-api/dist/main.js' | grep -v grep | awk '{print $2}'");
|
||||
foreach(explode("\n", trim($pids)) as $pid) {
|
||||
if ($pid) {
|
||||
posix_kill((int)$pid, 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# set "Allow Remote Access" to "No" and sign out from Unraid Connect
|
||||
if ($msini !== false) {
|
||||
if (!empty($msini['remote']['username'])) {
|
||||
$var = parse_ini_file("/var/local/emhttp/var.ini");
|
||||
$keyfile = @file_get_contents($var['regFILE']);
|
||||
if ($keyfile !== false) {
|
||||
echo "\nSigning out of Unraid Connect\n";
|
||||
$ch = curl_init('https://keys.lime-technology.com/account/server/unregister');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, ['keyfile' => @base64_encode($keyfile)]);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
}
|
||||
|
||||
# remove myservers.cfg
|
||||
unlink('/boot/config/plugins/dynamix.my.servers/myservers.cfg');
|
||||
|
||||
# reload nginx to disable Remote Access
|
||||
echo "\n⚠️ Reloading Web Server. If this window stops updating for two minutes please close it.\n";
|
||||
exec("/etc/rc.d/rc.nginx reload &>/dev/null");
|
||||
}
|
||||
exit(0);
|
||||
]]>
|
||||
</INLINE>
|
||||
</FILE>
|
||||
|
||||
<!-- disable features when upgrading on unsupported OS versions -->
|
||||
<!-- duplicated from above because the script can't distinguish between install and remove -->
|
||||
<!-- NOTE: this script is PHP not bash -->
|
||||
<!-- Cleanup for install on unsupported OS -->
|
||||
<FILE Run="/usr/bin/php" Method="install">
|
||||
<INLINE>
|
||||
<![CDATA[
|
||||
<?
|
||||
$ver = @parse_ini_file('/etc/unraid-version', true)['version'];
|
||||
$msini = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true);
|
||||
<?php
|
||||
// Check Unraid version
|
||||
$version = @parse_ini_file('/etc/unraid-version', true)['version'];
|
||||
|
||||
// exit this install block if NOT isUnsupportedVersion
|
||||
// must be 6.12.0 or higher (not 6.12.0-[beta|rc])
|
||||
if (version_compare($ver,'6.12.0','>=')) {
|
||||
// Check if this is a supported version
|
||||
// - Must be 6.12.0 or higher
|
||||
// - Must not be a 6.12.0 beta/rc version
|
||||
$is_stable_6_12_or_higher = version_compare($version, '6.12.0', '>=') && !preg_match('/^6\\.12\\.0-/', $version);
|
||||
|
||||
if ($is_stable_6_12_or_higher) {
|
||||
echo "Running on supported version {$version}, skipping cleanup\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
echo "**********************************\n";
|
||||
echo "🧹 CLEANING UP - may take a minute\n";
|
||||
echo "**********************************\n";
|
||||
echo "Running on unsupported version {$version}, performing cleanup\n";
|
||||
echo "Running cleanup operations...\n";
|
||||
include_once("/usr/local/emhttp/plugins/dynamix.my.servers/scripts/cleanup_operations.php");
|
||||
|
||||
if (file_exists("/boot/.git")) {
|
||||
if (file_exists("/etc/rc.d/rc.flash_backup")) {
|
||||
# stop flash backup service
|
||||
echo "\nStopping flash backup service. Please wait…";
|
||||
exec("/etc/rc.d/rc.flash_backup stop &>/dev/null");
|
||||
}
|
||||
if (file_exists("/usr/local/emhttp/plugins/dynamix.my.servers/include/UpdateFlashBackup.php")) {
|
||||
# deactivate and delete local flash backup
|
||||
echo "\nDeactivating flash backup. Please wait…";
|
||||
passthru("/usr/bin/php /usr/local/emhttp/plugins/dynamix.my.servers/include/UpdateFlashBackup.php deactivate");
|
||||
}
|
||||
}
|
||||
|
||||
# set "Allow Remote Access" to "No" and sign out from Unraid Connect
|
||||
if ($msini !== false) {
|
||||
# stop unraid-api
|
||||
echo "\nStopping unraid-api. Please wait…";
|
||||
$output = shell_exec("/etc/rc.d/rc.unraid-api stop --delete 2>&1'");
|
||||
if (!$output) {
|
||||
echo "Waiting for unraid-api to stop...\n";
|
||||
sleep(5); // Give it a few seconds to fully stop
|
||||
}
|
||||
echo "Stopped unraid-api: $output";
|
||||
|
||||
if (!empty($msini['remote']['username'])) {
|
||||
$var = parse_ini_file("/var/local/emhttp/var.ini");
|
||||
$keyfile = @file_get_contents($var['regFILE']);
|
||||
if ($keyfile !== false) {
|
||||
echo "\nSigning out of Unraid Connect\n";
|
||||
$ch = curl_init('https://keys.lime-technology.com/account/server/unregister');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, ['keyfile' => @base64_encode($keyfile)]);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
}
|
||||
|
||||
# remove myservers.cfg
|
||||
unlink('/boot/config/plugins/dynamix.my.servers/myservers.cfg');
|
||||
|
||||
# reload nginx to disable Remote Access
|
||||
echo "\n⚠️ Reloading Web Server. If this window stops updating for two minutes please close it.\n";
|
||||
exec("/etc/rc.d/rc.nginx reload &>/dev/null");
|
||||
}
|
||||
exit(0);
|
||||
]]>
|
||||
</INLINE>
|
||||
@@ -384,6 +258,7 @@ if [ -f /tmp/restore-files-dynamix-unraid-net ]; then
|
||||
"/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php"
|
||||
"/usr/local/emhttp/plugins/dynamix/include/ProvisionCert.php"
|
||||
"/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php"
|
||||
"/usr/local/emhttp/plugins/dynamix/include/ReplaceKey.php"
|
||||
"/usr/local/emhttp/plugins/dynamix/include/Wrappers.php"
|
||||
"/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page"
|
||||
"/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page"
|
||||
@@ -391,6 +266,7 @@ if [ -f /tmp/restore-files-dynamix-unraid-net ]; then
|
||||
"/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/showchanges"
|
||||
"/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck"
|
||||
"/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php"
|
||||
"/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheckExec.php"
|
||||
"/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page"
|
||||
"/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page"
|
||||
"/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page"
|
||||
@@ -505,10 +381,12 @@ echo
|
||||
preserveFilesDirs=(
|
||||
"move:/usr/local/emhttp/plugins/dynamix/Registration.page:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix/include/ReplaceKey.php:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheckExec.php:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page:skip"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page:skip"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page:preventDowngrade"
|
||||
@@ -525,6 +403,7 @@ preserveFilesDirs=(
|
||||
preserveAction() {
|
||||
local action="$1"
|
||||
local path="$2"
|
||||
local preventType="$3" # preventDowngrade or skip
|
||||
|
||||
if [[ "$action" == "move" ]]; then
|
||||
[[ -f "$path" ]] && mv -f "$path" "$path-"
|
||||
@@ -621,6 +500,7 @@ CFG_OLD=/boot/config/plugins/Unraid.net
|
||||
CFG_NEW=/boot/config/plugins/dynamix.my.servers
|
||||
[[ -d "$CFG_OLD" ]] && [[ ! -d "$CFG_NEW" ]] && mv "$CFG_OLD" "$CFG_NEW"
|
||||
|
||||
|
||||
# relax restrictions on built-in Firefox so it can sign in to Unraid Connect
|
||||
# brings older versions of Unraid in sync with 6.12.0
|
||||
# no need to restore original file on uninstall
|
||||
|
||||
@@ -391,6 +391,9 @@ if [ "$CHOWN" = "y" ]; then
|
||||
fi
|
||||
|
||||
$SUDO find . -type d -exec chmod 755 {} + || exit 1
|
||||
$SUDO find . -type d -name scripts -exec find {} -type f \; | while read -r file; do
|
||||
$SUDO chmod +x "$file"
|
||||
done
|
||||
$SUDO find . -exec chown 0:0 {} + || exit 1
|
||||
set +e
|
||||
fi
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
Menu="UNRAID-OS"
|
||||
Title="Log Viewer (new)"
|
||||
Icon="icon-log"
|
||||
Tag="list"
|
||||
---
|
||||
<?php
|
||||
/* Copyright 2005-2023, Lime Technology
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License version 2,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*/
|
||||
?>
|
||||
<unraid-i18n-host>
|
||||
<unraid-log-viewer></unraid-log-viewer>
|
||||
</unraid-i18n-host>
|
||||
@@ -14,6 +14,9 @@ Tag="pencil"
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*/
|
||||
require_once "$docroot/plugins/dynamix/include/ReplaceKey.php";
|
||||
$replaceKey = new ReplaceKey();
|
||||
$replaceKey->check(true);
|
||||
?>
|
||||
<unraid-i18n-host>
|
||||
<unraid-registration></unraid-registration>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
// Script Name: cleanup_operations.php
|
||||
// Purpose: Handles cleanup operations for both install on unsupported OS and during removal
|
||||
//
|
||||
// Usage:
|
||||
// ./cleanup_operations.php
|
||||
// ./cleanup_operations.php --debug
|
||||
|
||||
// Parse command line arguments
|
||||
$debug = false;
|
||||
|
||||
if (isset($argv)) {
|
||||
foreach ($argv as $arg) {
|
||||
if ($arg === '--debug') {
|
||||
$debug = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug function
|
||||
function debug_log($message) {
|
||||
global $debug;
|
||||
if ($debug) {
|
||||
echo "[DEBUG] [cleanup_operations]: $message\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Get Unraid version and myservers config
|
||||
$ver = @parse_ini_file('/etc/unraid-version', true)['version'];
|
||||
$msini = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true);
|
||||
|
||||
debug_log("Unraid version: $ver");
|
||||
debug_log("myservers.cfg exists: " . ($msini !== false ? "Yes" : "No"));
|
||||
|
||||
echo "\n";
|
||||
echo "**********************************\n";
|
||||
echo "🧹 CLEANING UP - may take a minute\n";
|
||||
echo "**********************************\n";
|
||||
|
||||
if (file_exists("/boot/.git")) {
|
||||
if (file_exists("/etc/rc.d/rc.flash_backup")) {
|
||||
# stop flash backup service
|
||||
echo "\nStopping flash backup service. Please wait…";
|
||||
exec("/etc/rc.d/rc.flash_backup stop &>/dev/null");
|
||||
}
|
||||
if (file_exists("/usr/local/emhttp/plugins/dynamix.my.servers/include/UpdateFlashBackup.php")) {
|
||||
# deactivate and delete local flash backup
|
||||
echo "\nDeactivating flash backup. Please wait…";
|
||||
passthru("/usr/bin/php /usr/local/emhttp/plugins/dynamix.my.servers/include/UpdateFlashBackup.php deactivate");
|
||||
}
|
||||
}
|
||||
|
||||
# set "Allow Remote Access" to "No" and sign out from Unraid Connect
|
||||
if ($msini !== false) {
|
||||
# stop unraid-api
|
||||
echo "\nStopping unraid-api. Please wait…";
|
||||
$output = shell_exec("/etc/rc.d/rc.unraid-api stop --delete 2>&1");
|
||||
if (!$output) {
|
||||
echo "Waiting for unraid-api to stop...\n";
|
||||
sleep(5); // Give it a few seconds to fully stop
|
||||
}
|
||||
echo "Stopped unraid-api: $output";
|
||||
|
||||
if (!empty($msini['remote']['username'])) {
|
||||
$var = parse_ini_file("/var/local/emhttp/var.ini");
|
||||
$keyfile = @file_get_contents($var['regFILE']);
|
||||
if ($keyfile !== false) {
|
||||
echo "\nSigning out of Unraid Connect\n";
|
||||
$ch = curl_init('https://keys.lime-technology.com/account/server/unregister');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, ['keyfile' => @base64_encode($keyfile)]);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
}
|
||||
|
||||
# remove myservers.cfg
|
||||
unlink('/boot/config/plugins/dynamix.my.servers/myservers.cfg');
|
||||
|
||||
# reload nginx to disable Remote Access
|
||||
echo "\n⚠️ Reloading Web Server. If this window stops updating for two minutes please close it.\n";
|
||||
exec("/etc/rc.d/rc.nginx reload &>/dev/null");
|
||||
}
|
||||
|
||||
exit(0);
|
||||
0
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/scripts/gitflash_log
Normal file → Executable file
0
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/scripts/gitflash_log
Normal file → Executable file
@@ -16,11 +16,12 @@ Tag="upload"
|
||||
/**
|
||||
* @note icon-update is rotated via CSS in myservers1.php
|
||||
*/
|
||||
require_once "$docroot/plugins/dynamix/include/ReplaceKey.php";
|
||||
$replaceKey = new ReplaceKey();
|
||||
$replaceKey->check();
|
||||
|
||||
require_once "$docroot/plugins/dynamix.my.servers/include/reboot-details.php";
|
||||
// Create an instance of the RebootDetails class
|
||||
$rebootDetails = new RebootDetails();
|
||||
// Get the current reboot details if there are any
|
||||
$rebootDetails->setPrevious();
|
||||
|
||||
$serverNameEscaped = htmlspecialchars(str_replace(' ', '_', strtolower($var['NAME'])));
|
||||
|
||||
@@ -13,6 +13,10 @@ Tag="upload"
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*/
|
||||
require_once "$docroot/plugins/dynamix/include/ReplaceKey.php";
|
||||
$replaceKey = new ReplaceKey();
|
||||
$replaceKey->check();
|
||||
|
||||
require_once "$docroot/plugins/dynamix.my.servers/include/reboot-details.php";
|
||||
$rebootDetails = new RebootDetails();
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* This file exists to maintain separation of concerns between UnraidCheck and ReplaceKey.
|
||||
* Instead of combining both classes directly, we utilize the unraidcheck script which already
|
||||
* handles both operations in a simplified manner.
|
||||
*
|
||||
* It's called via the WebguiCheckForUpdate function in composables/services/webgui.ts of the web components.
|
||||
* Handles WebguiUnraidCheckExecPayload interface parameters:
|
||||
* - altUrl?: string
|
||||
* - json?: boolean
|
||||
*/
|
||||
class UnraidCheckExec
|
||||
{
|
||||
private const SCRIPT_PATH = '/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck';
|
||||
private const ALLOWED_DOMAIN = 'releases.unraid.net';
|
||||
|
||||
private function setupEnvironment(): void
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('Content-Security-Policy: default-src \'none\'');
|
||||
|
||||
$params = [
|
||||
'json' => 'true',
|
||||
];
|
||||
|
||||
if (isset($_GET['altUrl'])) {
|
||||
$url = filter_var($_GET['altUrl'], FILTER_VALIDATE_URL);
|
||||
if ($url !== false) {
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||
|
||||
if ($host && $scheme === 'https' && (
|
||||
$host === self::ALLOWED_DOMAIN ||
|
||||
str_ends_with($host, '.' . self::ALLOWED_DOMAIN)
|
||||
)) {
|
||||
$params['altUrl'] = $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
putenv('QUERY_STRING=' . http_build_query($params));
|
||||
}
|
||||
|
||||
public function execute(): string
|
||||
{
|
||||
// Validate script with all necessary permissions
|
||||
if (!is_file(self::SCRIPT_PATH) ||
|
||||
!is_readable(self::SCRIPT_PATH) ||
|
||||
!is_executable(self::SCRIPT_PATH)) {
|
||||
throw new RuntimeException('Script not found or not executable');
|
||||
}
|
||||
|
||||
$this->setupEnvironment();
|
||||
$output = [];
|
||||
$command = escapeshellcmd(self::SCRIPT_PATH);
|
||||
if (exec($command, $output) === false) {
|
||||
throw new RuntimeException('Script execution failed');
|
||||
}
|
||||
|
||||
return implode("\n", $output);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
$checker = new UnraidCheckExec();
|
||||
echo $checker->execute();
|
||||
9
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck
Normal file → Executable file
9
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck
Normal file → Executable file
@@ -13,7 +13,14 @@
|
||||
?>
|
||||
<?
|
||||
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
||||
require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
|
||||
|
||||
require_once "$docroot/plugins/dynamix/include/ReplaceKey.php";
|
||||
$replaceKey = new ReplaceKey();
|
||||
$replaceKey->check();
|
||||
|
||||
// utilized by UnraidCheckExec.php to have UnraidCheck.php return a json response when this script is called directly
|
||||
parse_str(getenv('QUERY_STRING') ?? '', $_GET);
|
||||
|
||||
require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
|
||||
$unraidOsCheck = new UnraidOsCheck();
|
||||
$unraidOsCheck->checkForUpdate();
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
||||
|
||||
class ReplaceKey
|
||||
{
|
||||
private const KEY_SERVER_URL = 'https://keys.lime-technology.com';
|
||||
|
||||
private $docroot;
|
||||
private $var;
|
||||
private $guid;
|
||||
private $keyfile;
|
||||
private $regExp;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->docroot = $GLOBALS['docroot'] ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
||||
|
||||
$this->var = (array)@parse_ini_file('/var/local/emhttp/var.ini');
|
||||
$this->guid = @$this->var['regGUID'] ?? null;
|
||||
|
||||
$keyfileBase64 = empty($this->var['regFILE']) ? null : @file_get_contents($this->var['regFILE']);
|
||||
if ($keyfileBase64 !== false) {
|
||||
$keyfileBase64 = @base64_encode($keyfileBase64);
|
||||
$this->keyfile = str_replace(['+', '/', '='], ['-', '_', ''], trim($keyfileBase64));
|
||||
}
|
||||
|
||||
$this->regExp = @$this->var['regExp'] ?? null;
|
||||
}
|
||||
|
||||
private function request($url, $method, $payload = null, $headers = null)
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
|
||||
// Set the request method
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
// store the response in a variable instead of printing it
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
// Set the payload if present
|
||||
if ($payload !== null) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
}
|
||||
|
||||
if ($headers !== null) {
|
||||
// Set the headers
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
}
|
||||
|
||||
// Set additional options as needed
|
||||
|
||||
// Execute the request
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// Check for errors
|
||||
if (curl_errno($ch)) {
|
||||
$error = [
|
||||
'heading' => 'CurlError',
|
||||
'message' => curl_error($ch),
|
||||
'level' => 'error',
|
||||
'ref' => 'curlError',
|
||||
'type' => 'request',
|
||||
];
|
||||
// @todo store error
|
||||
}
|
||||
|
||||
// Close the cURL session
|
||||
curl_close($ch);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function validateGuid()
|
||||
{
|
||||
$headers = [
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
];
|
||||
|
||||
$params = [
|
||||
'guid' => $this->guid,
|
||||
'keyfile' => $this->keyfile,
|
||||
];
|
||||
|
||||
/**
|
||||
* returns {JSON}
|
||||
* hasNewerKeyfile : boolean;
|
||||
* purchaseable: true;
|
||||
* registered: false;
|
||||
* replaceable: false;
|
||||
* upgradeable: false;
|
||||
* upgradeAllowed: string[];
|
||||
* updatesRenewable: false;
|
||||
*/
|
||||
$response = $this->request(
|
||||
self::KEY_SERVER_URL . '/validate/guid',
|
||||
'POST',
|
||||
http_build_query($params),
|
||||
$headers,
|
||||
);
|
||||
|
||||
// Handle the response as needed (parsing JSON, etc.)
|
||||
$decodedResponse = json_decode($response, true);
|
||||
|
||||
if (!empty($decodedResponse)) {
|
||||
return $decodedResponse;
|
||||
}
|
||||
|
||||
// @todo save error response somewhere
|
||||
return [];
|
||||
}
|
||||
|
||||
private function getLatestKey()
|
||||
{
|
||||
$headers = [
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
];
|
||||
|
||||
$params = [
|
||||
'keyfile' => $this->keyfile,
|
||||
];
|
||||
|
||||
/**
|
||||
* returns {JSON}
|
||||
* license: string;
|
||||
*/
|
||||
$response = $this->request(
|
||||
self::KEY_SERVER_URL . '/key/latest',
|
||||
'POST',
|
||||
http_build_query($params),
|
||||
$headers,
|
||||
);
|
||||
|
||||
// Handle the response as needed (parsing JSON, etc.)
|
||||
$decodedResponse = json_decode($response, true);
|
||||
|
||||
if (!empty($decodedResponse) && !empty($decodedResponse['license'])) {
|
||||
return $decodedResponse['license'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function installNewKey($key)
|
||||
{
|
||||
require_once "$this->docroot/webGui/include/InstallKey.php";
|
||||
|
||||
$KeyInstaller = new KeyInstaller();
|
||||
$installResponse = $KeyInstaller->installKey($key);
|
||||
|
||||
$installSuccess = false;
|
||||
|
||||
if (!empty($installResponse)) {
|
||||
$decodedResponse = json_decode($installResponse, true);
|
||||
if (isset($decodedResponse['error'])) {
|
||||
$this->writeJsonFile(
|
||||
'/tmp/ReplaceKey/error.json',
|
||||
[
|
||||
'error' => $decodedResponse['error'],
|
||||
'ts' => time(),
|
||||
]
|
||||
);
|
||||
$installSuccess = false;
|
||||
} else {
|
||||
$installSuccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
$keyType = basename($key, '.key');
|
||||
$output = isset($GLOBALS['notify']) ? _var($GLOBALS['notify'],'plugin') : '';
|
||||
$script = '/usr/local/emhttp/webGui/scripts/notify';
|
||||
|
||||
if ($installSuccess) {
|
||||
$event = "Installed New $keyType License";
|
||||
$subject = "Your new $keyType license key has been automatically installed";
|
||||
$description = "";
|
||||
$importance = "normal $output";
|
||||
} else {
|
||||
$event = "Failed to Install New $keyType License";
|
||||
$subject = "Failed to automatically install your new $keyType license key";
|
||||
$description = isset($decodedResponse['error']) ? $decodedResponse['error'] : "Unknown error occurred";
|
||||
$importance = "alert $output";
|
||||
}
|
||||
|
||||
exec("$script -e ".escapeshellarg($event)." -s ".escapeshellarg($subject)." -d ".escapeshellarg($description)." -i ".escapeshellarg($importance)." -l '/Tools/Registration' -x");
|
||||
|
||||
return $installSuccess;
|
||||
}
|
||||
|
||||
private function writeJsonFile($file, $data)
|
||||
{
|
||||
if (!is_dir(dirname($file))) {
|
||||
mkdir(dirname($file));
|
||||
}
|
||||
|
||||
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
public function check(bool $forceCheck = false)
|
||||
{
|
||||
// we don't need to check
|
||||
if (empty($this->guid) || empty($this->keyfile) || empty($this->regExp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're within the 7-day window before and after regExp
|
||||
$now = time();
|
||||
$sevenDaysBefore = strtotime('-7 days', $this->regExp);
|
||||
$sevenDaysAfter = strtotime('+7 days', $this->regExp);
|
||||
|
||||
$isWithinWindow = ($now >= $sevenDaysBefore && $now <= $sevenDaysAfter);
|
||||
|
||||
if (!$forceCheck && !$isWithinWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// see if we have a new key
|
||||
$validateGuidResponse = $this->validateGuid();
|
||||
|
||||
$hasNewerKeyfile = @$validateGuidResponse['hasNewerKeyfile'] ?? false;
|
||||
if (!$hasNewerKeyfile) {
|
||||
return; // if there is no newer keyfile, we don't need to do anything
|
||||
}
|
||||
|
||||
$latestKey = $this->getLatestKey();
|
||||
if (!$latestKey) {
|
||||
// we supposedly have a new key, but didn't get it back…
|
||||
$this->writeJsonFile(
|
||||
'/tmp/ReplaceKey/error.json',
|
||||
[
|
||||
'error' => 'Failed to retrieve latest key after getting a `hasNewerKeyfile` in the validation response.',
|
||||
'ts' => time(),
|
||||
]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->installNewKey($latestKey);
|
||||
}
|
||||
}
|
||||
2608
pnpm-lock.yaml
generated
2608
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,18 @@ const preview: Preview = {
|
||||
},
|
||||
},
|
||||
},
|
||||
// Add decorator to include modals container in every story
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template: `
|
||||
<div>
|
||||
<div id="modals"></div>
|
||||
<story />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "default",
|
||||
"typescript": true,
|
||||
"tsConfigPath": "./tsconfig.json",
|
||||
"tailwind": {
|
||||
"config": "./tailwind.config.ts",
|
||||
"css": "./src/styles/globals.css",
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"framework": "vite",
|
||||
"aliases": {
|
||||
"components": "@/components/common",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
"composables": "@/composables",
|
||||
"utils": "@/lib/utils",
|
||||
"lib": "@/lib"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/ui",
|
||||
"version": "4.2.1",
|
||||
"version": "4.4.0",
|
||||
"private": true,
|
||||
"license": "GPL-2.0-only",
|
||||
"type": "module",
|
||||
@@ -44,14 +44,14 @@
|
||||
"@jsonforms/core": "^3.5.1",
|
||||
"@jsonforms/vue": "^3.5.1",
|
||||
"@jsonforms/vue-vanilla": "^3.5.1",
|
||||
"@vueuse/core": "^12.0.0",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"kebab-case": "^2.0.1",
|
||||
"lucide-vue-next": "^0.483.0",
|
||||
"radix-vue": "^1.9.13",
|
||||
"reka-ui": "^2.0.2",
|
||||
"shadcn-vue": "^0.11.3",
|
||||
"shadcn-vue": "^1.0.0",
|
||||
"reka-ui": "^2.1.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"vue-sonner": "^1.3.0"
|
||||
},
|
||||
@@ -77,7 +77,7 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"happy-dom": "^17.0.0",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "3.5.3",
|
||||
@@ -119,5 +119,5 @@
|
||||
"import": "./dist/theme/preset.js"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.6.4"
|
||||
"packageManager": "pnpm@10.6.5"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { computed } from 'vue';
|
||||
import { brandLoadingVariants, markAnimations } from './brand-loading.variants';
|
||||
import {
|
||||
brandLoadingVariants,
|
||||
markAnimations,
|
||||
type BrandLoadingVariants,
|
||||
} from './brand-loading.variants';
|
||||
|
||||
export interface Props {
|
||||
variant?: 'default' | 'black' | 'white';
|
||||
size?: 'sm' | 'md' | 'lg' | 'full';
|
||||
variant?: BrandLoadingVariants['variant'];
|
||||
size?: BrandLoadingVariants['size'];
|
||||
class?: string;
|
||||
title?: string;
|
||||
}
|
||||
@@ -22,7 +26,7 @@ const GRADIENT_COLORS = {
|
||||
default: { start: '#e32929', stop: '#ff8d30' },
|
||||
} as const;
|
||||
|
||||
const gradientColors = computed(() => GRADIENT_COLORS[props.variant]);
|
||||
const gradientColors = computed(() => GRADIENT_COLORS[props.variant as keyof typeof GRADIENT_COLORS]);
|
||||
|
||||
const classes = computed(() => {
|
||||
return cn(brandLoadingVariants({ variant: props.variant, size: props.size }), props.class);
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
export const brandLoadingVariants = cva('inline-flex items-center justify-center w-full h-full aspect-[7/4]', {
|
||||
variants: {
|
||||
variant: {
|
||||
default: '',
|
||||
black: 'text-black fill-black',
|
||||
white: 'text-white fill-white',
|
||||
export const brandLoadingVariants = cva(
|
||||
'inline-flex items-center justify-center w-full h-full aspect-[7/4]',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: '',
|
||||
black: 'text-black fill-black',
|
||||
white: 'text-white fill-white',
|
||||
},
|
||||
size: {
|
||||
sm: 'w-12',
|
||||
md: 'w-16',
|
||||
lg: 'w-20',
|
||||
full: 'w-full',
|
||||
custom: '',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
sm: 'w-12',
|
||||
md: 'w-16',
|
||||
lg: 'w-20',
|
||||
full: 'w-full',
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const markAnimations = {
|
||||
mark_2_4: 'animate-mark-2',
|
||||
@@ -25,3 +29,5 @@ export const markAnimations = {
|
||||
mark_6_8: 'animate-mark-6',
|
||||
mark_7: 'animate-mark-7',
|
||||
};
|
||||
|
||||
export type BrandLoadingVariants = VariantProps<typeof brandLoadingVariants>;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuRoot,
|
||||
useForwardPropsEmits,
|
||||
type DropdownMenuRootEmits,
|
||||
type DropdownMenuRootProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from 'reka-ui';
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>();
|
||||
const emits = defineEmits<DropdownMenuRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
const forwarded = useForwardPropsEmits<DropdownMenuRootProps, 'update:open'>(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
DropdownMenuArrow as RekaDropdownMenuArrow,
|
||||
useForwardProps,
|
||||
type DropdownMenuArrowProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<DropdownMenuArrowProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RekaDropdownMenuArrow
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'fill-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,18 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Check } from 'lucide-vue-next';
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
type DropdownMenuCheckboxItemEmits,
|
||||
type DropdownMenuCheckboxItemProps,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { Check } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
type DropdownMenuCheckboxItemEmits,
|
||||
type DropdownMenuCheckboxItemProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>();
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import useTeleport from '@/composables/useTeleport';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
type DropdownMenuContentEmits,
|
||||
type DropdownMenuContentProps,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
type DropdownMenuContentEmits,
|
||||
type DropdownMenuContentProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
sideOffset: 4,
|
||||
class: undefined,
|
||||
}
|
||||
);
|
||||
const emits = defineEmits<DropdownMenuContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
@@ -28,17 +29,20 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuPortal :to="teleportTarget">
|
||||
<DropdownMenuContent
|
||||
v-bind="forwarded"
|
||||
side="bottom"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
'z-50 min-w-32 rounded-lg bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<div class="overflow-hidden">
|
||||
<slot />
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "radix-vue";
|
||||
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<DropdownMenuGroupProps>();
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
type DropdownMenuItemProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DropdownMenuItem, useForwardProps, type DropdownMenuItemProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuItemProps & { class?: HTMLAttributes["class"]; inset?: boolean }
|
||||
DropdownMenuItemProps & { class?: HTMLAttributes['class']; inset?: boolean }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
@@ -25,7 +21,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
props.class
|
||||
)
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuLabel,
|
||||
type DropdownMenuLabelProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DropdownMenuLabel, useForwardProps, type DropdownMenuLabelProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuLabelProps & { class?: HTMLAttributes["class"]; inset?: boolean }
|
||||
DropdownMenuLabelProps & { class?: HTMLAttributes['class']; inset?: boolean }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
@@ -23,9 +19,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||
<template>
|
||||
<DropdownMenuLabel
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)
|
||||
"
|
||||
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuLabel>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuRadioGroup,
|
||||
useForwardPropsEmits,
|
||||
type DropdownMenuRadioGroupEmits,
|
||||
type DropdownMenuRadioGroupProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from 'reka-ui';
|
||||
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>();
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
const forwarded = useForwardPropsEmits<DropdownMenuRadioGroupProps, 'update:modelValue'>(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Circle } from 'lucide-vue-next';
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioItem,
|
||||
useForwardPropsEmits,
|
||||
type DropdownMenuRadioItemEmits,
|
||||
type DropdownMenuRadioItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { Circle } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const emits = defineEmits<DropdownMenuRadioItemEmits>();
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuSeparator,
|
||||
type DropdownMenuSeparatorProps,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DropdownMenuSeparator, type DropdownMenuSeparatorProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuSeparatorProps & {
|
||||
class?: HTMLAttributes["class"];
|
||||
class?: HTMLAttributes['class'];
|
||||
}
|
||||
>();
|
||||
|
||||
@@ -20,8 +17,5 @@ const delegatedProps = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSeparator
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
|
||||
/>
|
||||
<DropdownMenuSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
useForwardPropsEmits,
|
||||
type DropdownMenuSubEmits,
|
||||
type DropdownMenuSubProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
} from 'reka-ui';
|
||||
|
||||
const props = defineProps<DropdownMenuSubProps>();
|
||||
const emits = defineEmits<DropdownMenuSubEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
const forwarded = useForwardPropsEmits<DropdownMenuSubProps, 'update:open'>(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
useForwardPropsEmits,
|
||||
type DropdownMenuSubContentEmits,
|
||||
type DropdownMenuSubContentProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>();
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuSubTrigger,
|
||||
type DropdownMenuSubTriggerProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { ChevronRight } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronRight } from 'lucide-vue-next';
|
||||
import { DropdownMenuSubTrigger, useForwardProps, type DropdownMenuSubTriggerProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownMenuTrigger, type DropdownMenuTriggerProps, useForwardProps } from 'radix-vue'
|
||||
import { DropdownMenuTrigger, useForwardProps, type DropdownMenuTriggerProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>()
|
||||
const props = defineProps<DropdownMenuTriggerProps>();
|
||||
|
||||
const forwardedProps = useForwardProps(props)
|
||||
const forwardedProps = useForwardProps<DropdownMenuTriggerProps>(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
export { DropdownMenuPortal } from 'radix-vue'
|
||||
export { default as DropdownMenu } from './DropdownMenu.vue';
|
||||
|
||||
export { default as DropdownMenu } from './DropdownMenu.vue'
|
||||
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
|
||||
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'
|
||||
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'
|
||||
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'
|
||||
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'
|
||||
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'
|
||||
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'
|
||||
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'
|
||||
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'
|
||||
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'
|
||||
export { default as DropdownMenuSub } from './DropdownMenuSub.vue'
|
||||
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'
|
||||
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'
|
||||
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue';
|
||||
export { default as DropdownMenuContent } from './DropdownMenuContent.vue';
|
||||
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue';
|
||||
export { default as DropdownMenuItem } from './DropdownMenuItem.vue';
|
||||
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue';
|
||||
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue';
|
||||
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue';
|
||||
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue';
|
||||
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue';
|
||||
export { default as DropdownMenuSub } from './DropdownMenuSub.vue';
|
||||
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue';
|
||||
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue';
|
||||
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue';
|
||||
export { default as DropdownMenuArrow } from './DropdownMenuArrow.vue';
|
||||
export { DropdownMenuPortal } from 'reka-ui';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverRootEmits, PopoverRootProps } from 'radix-vue'
|
||||
import { PopoverRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { PopoverRootEmits, PopoverRootProps } from 'reka-ui';
|
||||
import { PopoverRoot, useForwardPropsEmits } from 'reka-ui';
|
||||
|
||||
const props = defineProps<PopoverRootProps>()
|
||||
const emits = defineEmits<PopoverRootEmits>()
|
||||
const props = defineProps<PopoverRootProps>();
|
||||
const emits = defineEmits<PopoverRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
const forwarded = useForwardPropsEmits<PopoverRootProps, 'update:open'>(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverContentEmits, PopoverContentProps } from 'radix-vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
|
||||
PopoverPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed } from 'vue'
|
||||
type PopoverContentEmits,
|
||||
type PopoverContentProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
align: 'center',
|
||||
sideOffset: 4,
|
||||
},
|
||||
)
|
||||
const emits = defineEmits<PopoverContentEmits>()
|
||||
const props = withDefaults(defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
align: 'center',
|
||||
sideOffset: 4,
|
||||
});
|
||||
const emits = defineEmits<PopoverContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated
|
||||
})
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -38,12 +35,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
props.class,
|
||||
'z-50 w-72 rounded-md bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<PopoverArrow :rounded="true" class="fill-popover" />
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverTriggerProps } from 'radix-vue'
|
||||
import { PopoverTrigger } from 'radix-vue'
|
||||
import { PopoverTrigger, type PopoverTriggerProps } from 'reka-ui'
|
||||
|
||||
const props = defineProps<PopoverTriggerProps>()
|
||||
</script>
|
||||
|
||||
@@ -1,39 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted } from "vue";
|
||||
import {
|
||||
DialogRoot,
|
||||
useForwardPropsEmits,
|
||||
type DialogRootEmits,
|
||||
type DialogRootProps,
|
||||
} from "radix-vue";
|
||||
import { DialogRoot, useForwardPropsEmits, type DialogRootEmits, type DialogRootProps } from 'reka-ui';
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
|
||||
const MOBILE_VIEWPORT =
|
||||
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" as const;
|
||||
const MOBILE_VIEWPORT = 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0' as const;
|
||||
|
||||
const props = defineProps<DialogRootProps & { class?: string }>();
|
||||
const emits = defineEmits<DialogRootEmits>();
|
||||
|
||||
const getViewport = (): string => {
|
||||
return (
|
||||
document.querySelector('meta[name="viewport"]')?.getAttribute("content") ??
|
||||
"width=1300"
|
||||
);
|
||||
return document.querySelector('meta[name="viewport"]')?.getAttribute('content') ?? 'width=1300';
|
||||
};
|
||||
const updateViewport = (viewport: string): void => {
|
||||
if (window.innerWidth < 500) {
|
||||
const meta = document.querySelector('meta[name="viewport"]');
|
||||
if (meta) {
|
||||
meta.setAttribute("content", viewport);
|
||||
meta.setAttribute('content', viewport);
|
||||
} else {
|
||||
const meta = document.createElement("meta");
|
||||
meta.name = "viewport";
|
||||
const meta = document.createElement('meta');
|
||||
meta.name = 'viewport';
|
||||
meta.content = viewport;
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
const forwarded = useForwardPropsEmits(props, emits) as any;
|
||||
const initialViewport = ref(getViewport());
|
||||
const openListener = (opened: boolean) => {
|
||||
if (opened) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogClose, type DialogCloseProps } from 'radix-vue'
|
||||
import { DialogClose, type DialogCloseProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
const props = defineProps<DialogCloseProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { X } from 'lucide-vue-next';
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { X } from "lucide-vue-next";
|
||||
import { sheetVariants } from "./sheet.variants";
|
||||
import { cn } from "@/lib/utils";
|
||||
type DialogContentEmits,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import { sheetVariants } from './sheet.variants';
|
||||
|
||||
export interface SheetContentProps {
|
||||
side?: "top" | "bottom" | "left" | "right";
|
||||
padding?: "none" | "md";
|
||||
class?: HTMLAttributes["class"];
|
||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||
padding?: 'none' | 'md';
|
||||
class?: HTMLAttributes['class'];
|
||||
disabled?: boolean;
|
||||
forceMount?: boolean;
|
||||
to?: string | HTMLElement;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||
side: "right",
|
||||
padding: "md",
|
||||
side: 'right',
|
||||
padding: 'md',
|
||||
});
|
||||
|
||||
const emits = defineEmits<DialogContentEmits>();
|
||||
|
||||
const sheetClass = computed(() => {
|
||||
return cn(
|
||||
sheetVariants({ side: props.side, padding: props.padding }),
|
||||
props.class,
|
||||
);
|
||||
return cn(sheetVariants({ side: props.side, padding: props.padding }), props.class);
|
||||
});
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { DialogDescription, type DialogDescriptionProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DialogDescription, type DialogDescriptionProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DialogDescriptionProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
@@ -15,10 +13,7 @@ const delegatedProps = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogDescription
|
||||
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<DialogDescription :class="cn('text-sm text-muted-foreground', props.class)" v-bind="delegatedProps">
|
||||
<slot />
|
||||
</DialogDescription>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { DialogTitle, type DialogTitleProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DialogTitle, type DialogTitleProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DialogTitleProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
@@ -15,10 +13,7 @@ const delegatedProps = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTitle
|
||||
:class="cn('text-lg font-medium text-foreground', props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<DialogTitle :class="cn('text-lg font-medium text-foreground', props.class)" v-bind="delegatedProps">
|
||||
<slot />
|
||||
</DialogTitle>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogTrigger, type DialogTriggerProps } from 'radix-vue'
|
||||
import { DialogTrigger, type DialogTriggerProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
const props = defineProps<DialogTriggerProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { TabsRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { TabsRootEmits, TabsRootProps } from 'radix-vue'
|
||||
import { TabsRoot, useForwardPropsEmits } from 'radix-vue';
|
||||
import type { TabsRootEmits, TabsRootProps } from 'radix-vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<TabsRootProps>()
|
||||
const emits = defineEmits<TabsRootEmits>()
|
||||
const props = defineProps<TabsRootProps>();
|
||||
const emits = defineEmits<TabsRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
const forwarded = useForwardPropsEmits(props, emits) as TabsRootProps & HTMLAttributes;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { TooltipRoot, type TooltipRootEmits, type TooltipRootProps, useForwardPropsEmits } from 'radix-vue'
|
||||
import {
|
||||
TooltipRoot,
|
||||
useForwardPropsEmits,
|
||||
type TooltipRootEmits,
|
||||
type TooltipRootProps,
|
||||
} from 'radix-vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<TooltipRootProps>()
|
||||
const emits = defineEmits<TooltipRootEmits>()
|
||||
const props = defineProps<TooltipRootProps>();
|
||||
const emits = defineEmits<TooltipRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
const forwarded = useForwardPropsEmits(props, emits) as TooltipRootProps & HTMLAttributes;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectRootEmits, SelectRootProps } from 'radix-vue'
|
||||
import { SelectRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { SelectRootEmits, SelectRootProps } from 'reka-ui';
|
||||
import { SelectRoot, useForwardPropsEmits } from 'reka-ui';
|
||||
|
||||
const props = defineProps<SelectRootProps>()
|
||||
const emits = defineEmits<SelectRootEmits>()
|
||||
const props = defineProps<SelectRootProps>();
|
||||
const emits = defineEmits<SelectRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
const forwarded = useForwardPropsEmits<SelectRootProps, 'update:modelValue'>(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import useTeleport from '@/composables/useTeleport';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
SelectContent,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
useForwardPropsEmits,
|
||||
type SelectContentEmits,
|
||||
type SelectContentProps,
|
||||
} from 'radix-vue';
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import { SelectScrollDownButton, SelectScrollUpButton } from '.';
|
||||
|
||||
@@ -15,21 +16,14 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
SelectContentProps & {
|
||||
class?: HTMLAttributes['class'];
|
||||
disabled?: boolean;
|
||||
forceMount?: boolean;
|
||||
to?: string | HTMLElement | Element;
|
||||
}
|
||||
>(),
|
||||
{
|
||||
position: 'popper',
|
||||
class: undefined,
|
||||
to: '#modals',
|
||||
}
|
||||
);
|
||||
const { teleportTarget } = useTeleport();
|
||||
|
||||
const props = withDefaults(defineProps<SelectContentProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
forceMount: false,
|
||||
position: 'popper',
|
||||
to: undefined,
|
||||
});
|
||||
|
||||
const emits = defineEmits<SelectContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
@@ -42,7 +36,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectPortal :disabled="disabled" :force-mount="forceMount" :to="to as HTMLElement">
|
||||
<SelectPortal :force-mount="forceMount" :to="teleportTarget">
|
||||
<SelectContent
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="
|
||||
@@ -60,7 +54,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[--radix-select-trigger-height] w-full min-w-[--radix-select-trigger-width]'
|
||||
'h-[--reka-select-trigger-height] w-full min-w-[--reka-select-trigger-width]'
|
||||
)
|
||||
"
|
||||
>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { SelectGroup, type SelectGroupProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SelectGroup, type SelectGroupProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
SelectGroupProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Check } from 'lucide-vue-next';
|
||||
import {
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
type SelectItemProps,
|
||||
SelectItemText,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { Check } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
type SelectItemProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
SelectItemProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { SelectItemText, type SelectItemTextProps } from 'radix-vue'
|
||||
import { SelectItemText, type SelectItemTextProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<SelectItemTextProps>()
|
||||
const props = defineProps<SelectItemTextProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { SelectLabel, type SelectLabelProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SelectLabel, type SelectLabelProps } from 'reka-ui';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
SelectLabelProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectLabel
|
||||
:class="cn('py-1.5 pl-8 pr-2 text-sm font-semibold', props.class)"
|
||||
>
|
||||
<SelectLabel :class="cn('py-1.5 pl-8 pr-2 text-sm font-semibold', props.class)">
|
||||
<slot />
|
||||
</SelectLabel>
|
||||
</template>
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
SelectScrollDownButton,
|
||||
type SelectScrollDownButtonProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { ChevronDown } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { SelectScrollDownButton, useForwardProps, type SelectScrollDownButtonProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
SelectScrollDownButtonProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
@@ -24,9 +18,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||
<template>
|
||||
<SelectScrollDownButton
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn('flex cursor-default items-center justify-center py-1', props.class)
|
||||
"
|
||||
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<ChevronDown class="h-4 w-4" />
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
SelectScrollUpButton,
|
||||
type SelectScrollUpButtonProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { ChevronUp } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronUp } from 'lucide-vue-next';
|
||||
import { SelectScrollUpButton, useForwardProps, type SelectScrollUpButtonProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
SelectScrollUpButtonProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
@@ -24,9 +18,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||
<template>
|
||||
<SelectScrollUpButton
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn('flex cursor-default items-center justify-center py-1', props.class)
|
||||
"
|
||||
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<ChevronUp class="h-4 w-4" />
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { SelectSeparator, type SelectSeparatorProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SelectSeparator, type SelectSeparatorProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
SelectSeparatorProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
@@ -15,8 +13,5 @@ const delegatedProps = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectSeparator
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
|
||||
/>
|
||||
<SelectSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
SelectIcon,
|
||||
SelectTrigger,
|
||||
type SelectTriggerProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { ChevronDown } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { SelectIcon, SelectTrigger, useForwardProps, type SelectTriggerProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
SelectTriggerProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
@@ -27,7 +20,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-4.5 py-3 text-base ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { SelectValue, type SelectValueProps } from 'radix-vue'
|
||||
import { SelectValue, type SelectValueProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<SelectValueProps>()
|
||||
const props = defineProps<SelectValueProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export { default as Select } from './Select.vue'
|
||||
export { default as SelectValue } from './SelectValue.vue'
|
||||
export { default as SelectTrigger } from './SelectTrigger.vue'
|
||||
export { default as SelectContent } from './SelectContent.vue'
|
||||
export { default as SelectGroup } from './SelectGroup.vue'
|
||||
export { default as SelectItem } from './SelectItem.vue'
|
||||
export { default as SelectItemText } from './SelectItemText.vue'
|
||||
export { default as SelectLabel } from './SelectLabel.vue'
|
||||
export { default as SelectSeparator } from './SelectSeparator.vue'
|
||||
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue'
|
||||
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue'
|
||||
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue'
|
||||
export { default as SelectSeparator } from './SelectSeparator.vue'
|
||||
export { default as SelectTrigger } from './SelectTrigger.vue'
|
||||
export { default as SelectValue } from './SelectValue.vue'
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const useTeleport = () => {
|
||||
const teleportTarget = ref<string | HTMLElement | Element>("#modals");
|
||||
const teleportTarget = ref<string | HTMLElement>('#modals');
|
||||
|
||||
const determineTeleportTarget = () => {
|
||||
const myModalsComponent = document.querySelector("unraid-modals");
|
||||
const myModalsComponent = document.querySelector('unraid-modals');
|
||||
if (!myModalsComponent?.shadowRoot) return;
|
||||
|
||||
const potentialTarget = myModalsComponent.shadowRoot.querySelector("#modals");
|
||||
const potentialTarget = myModalsComponent.shadowRoot.querySelector('#modals');
|
||||
if (!potentialTarget) return;
|
||||
|
||||
teleportTarget.value = potentialTarget;
|
||||
console.log("[determineTeleportTarget] teleportTarget", teleportTarget.value);
|
||||
teleportTarget.value = potentialTarget as HTMLElement;
|
||||
console.log('[determineTeleportTarget] teleportTarget', teleportTarget.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -10,11 +8,10 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/form/select';
|
||||
import useTeleport from '@/composables/useTeleport';
|
||||
import { useJsonFormsControl } from '@jsonforms/vue';
|
||||
|
||||
import type { ControlElement } from '@jsonforms/core';
|
||||
import { useJsonFormsControl } from '@jsonforms/vue';
|
||||
import type { RendererProps } from '@jsonforms/vue';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import ControlLayout from './ControlLayout.vue';
|
||||
|
||||
const props = defineProps<RendererProps<ControlElement>>();
|
||||
@@ -29,8 +26,8 @@ const options = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
const onChange = (value: string) => {
|
||||
handleChange(control.value.path, value);
|
||||
const onChange = (value: unknown) => {
|
||||
handleChange(control.value.path, String(value));
|
||||
};
|
||||
|
||||
// Without this, the select dropdown will not be visible, unless it's already in a teleported context.
|
||||
@@ -55,7 +52,7 @@ const onSelectOpen = () => {
|
||||
<span v-else>{{ control.schema.default ?? 'Select an option' }}</span>
|
||||
</SelectTrigger>
|
||||
<!-- The content includes the selectable options -->
|
||||
<SelectContent :to="teleportTarget">
|
||||
<SelectContent :to="teleportTarget as HTMLElement">
|
||||
<SelectItem v-for="option in options" :key="option.value" :value="option.value">
|
||||
<SelectItemText>{{ option.label }}</SelectItemText>
|
||||
</SelectItem>
|
||||
|
||||
@@ -14,11 +14,13 @@ import { Badge, type BadgeProps } from '@/components/common/badge';
|
||||
import { Button, buttonVariants, type ButtonProps } from '@/components/common/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuArrow,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/common/dropdown-menu';
|
||||
import { Bar, Error, Spinner } from '@/components/common/loading';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/common/popover';
|
||||
import { ScrollArea, ScrollBar } from '@/components/common/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
@@ -90,23 +93,28 @@ export {
|
||||
CardWrapper,
|
||||
cn,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuArrow,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuPortal,
|
||||
Error,
|
||||
Input,
|
||||
Label,
|
||||
PageContainer,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
ScrollBar,
|
||||
ScrollArea,
|
||||
Select,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
@@ -34,6 +34,16 @@
|
||||
--ring: 0 0% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--chart-1: 12 76% 61%;
|
||||
|
||||
--chart-2: 173 58% 39%;
|
||||
|
||||
--chart-3: 197 37% 24%;
|
||||
|
||||
--chart-4: 43 74% 66%;
|
||||
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -65,6 +75,16 @@
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 0 0% 83.1%;
|
||||
|
||||
--chart-1: 220 70% 50%;
|
||||
|
||||
--chart-2: 160 60% 45%;
|
||||
|
||||
--chart-3: 30 80% 55%;
|
||||
|
||||
--chart-4: 280 65% 60%;
|
||||
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { MoreVertical } from "lucide-vue-next";
|
||||
|
||||
import Button from "../../../src/components/common/button/Button.vue";
|
||||
import DropdownMenu from "../../../src/components/common/dropdown-menu/DropdownMenu.vue";
|
||||
import DropdownMenuContent from "../../../src/components/common/dropdown-menu/DropdownMenuContent.vue";
|
||||
import DropdownMenuItem from "../../../src/components/common/dropdown-menu/DropdownMenuItem.vue";
|
||||
import DropdownMenuLabel from "../../../src/components/common/dropdown-menu/DropdownMenuLabel.vue";
|
||||
import DropdownMenuSeparator from "../../../src/components/common/dropdown-menu/DropdownMenuSeparator.vue";
|
||||
import DropdownMenuTrigger from "../../../src/components/common/dropdown-menu/DropdownMenuTrigger.vue";
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
import { MoreVertical } from 'lucide-vue-next';
|
||||
import Button from '../../../src/components/common/button/Button.vue';
|
||||
import DropdownMenu from '../../../src/components/common/dropdown-menu/DropdownMenu.vue';
|
||||
import DropdownMenuArrow from '../../../src/components/common/dropdown-menu/DropdownMenuArrow.vue';
|
||||
import DropdownMenuContent from '../../../src/components/common/dropdown-menu/DropdownMenuContent.vue';
|
||||
import DropdownMenuItem from '../../../src/components/common/dropdown-menu/DropdownMenuItem.vue';
|
||||
import DropdownMenuLabel from '../../../src/components/common/dropdown-menu/DropdownMenuLabel.vue';
|
||||
import DropdownMenuSeparator from '../../../src/components/common/dropdown-menu/DropdownMenuSeparator.vue';
|
||||
import DropdownMenuTrigger from '../../../src/components/common/dropdown-menu/DropdownMenuTrigger.vue';
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common/DropdownMenu",
|
||||
title: 'Components/Common/DropdownMenu',
|
||||
component: DropdownMenu,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
open: {
|
||||
control: 'boolean',
|
||||
description: 'Controls the open state of the dropdown menu',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof DropdownMenu>;
|
||||
|
||||
export default meta;
|
||||
@@ -21,6 +28,7 @@ type Story = StoryObj<typeof meta>;
|
||||
export const Dropdown: Story = {
|
||||
render: () => ({
|
||||
components: {
|
||||
DropdownMenuArrow,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
@@ -30,18 +38,21 @@ export const Dropdown: Story = {
|
||||
Button,
|
||||
},
|
||||
template: `
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="secondary">Open Menu</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div class="bg-gray-200 p-4 h-screen">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="secondary">Open Menu</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
<DropdownMenuArrow />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -58,7 +69,7 @@ export const IconDropdown: Story = {
|
||||
Button,
|
||||
MoreVertical,
|
||||
},
|
||||
template: `
|
||||
template: `
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="ghost" size="icon">
|
||||
@@ -71,8 +82,9 @@ export const IconDropdown: Story = {
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Duplicate</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
<DropdownMenuArrow />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
64
unraid-ui/stories/components/common/Popover.stories.ts
Normal file
64
unraid-ui/stories/components/common/Popover.stories.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
import { Button } from '../../../src/components/common/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../../../src/components/common/popover';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Common/Popover',
|
||||
component: Popover,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
open: {
|
||||
control: 'boolean',
|
||||
description: 'Controls the open state of the popover',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Popover>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { Popover, PopoverContent, PopoverTrigger, Button },
|
||||
template: `
|
||||
<div class="p-8">
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button>Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" align="center" :side-offset="8">
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-medium leading-none">Dimensions</h4>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Set the dimensions for the layer.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithCustomPosition: Story = {
|
||||
render: () => ({
|
||||
components: { Popover, PopoverContent, PopoverTrigger, Button },
|
||||
template: `
|
||||
<div class="p-8">
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button>Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" align="start" :side-offset="8">
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-medium leading-none">Custom Position</h4>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
This popover is positioned on the right side with custom offset.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -1,24 +1,37 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import SheetComponent from "../../../src/components/common/sheet/Sheet.vue";
|
||||
import SheetTrigger from "../../../src/components/common/sheet/SheetTrigger.vue";
|
||||
import SheetContent from "../../../src/components/common/sheet/SheetContent.vue";
|
||||
import SheetHeader from "../../../src/components/common/sheet/SheetHeader.vue";
|
||||
import SheetTitle from "../../../src/components/common/sheet/SheetTitle.vue";
|
||||
import SheetDescription from "../../../src/components/common/sheet/SheetDescription.vue";
|
||||
import SheetFooter from "../../../src/components/common/sheet/SheetFooter.vue";
|
||||
import Button from "../../../src/components/common/button/Button.vue";
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
import { h } from 'vue';
|
||||
import Button from '../../../src/components/common/button/Button.vue';
|
||||
import SheetComponent from '../../../src/components/common/sheet/Sheet.vue';
|
||||
import SheetContent from '../../../src/components/common/sheet/SheetContent.vue';
|
||||
import SheetDescription from '../../../src/components/common/sheet/SheetDescription.vue';
|
||||
import SheetFooter from '../../../src/components/common/sheet/SheetFooter.vue';
|
||||
import SheetHeader from '../../../src/components/common/sheet/SheetHeader.vue';
|
||||
import SheetTitle from '../../../src/components/common/sheet/SheetTitle.vue';
|
||||
import SheetTrigger from '../../../src/components/common/sheet/SheetTrigger.vue';
|
||||
import Select from '../../../src/components/form/select/Select.vue';
|
||||
import SelectContent from '../../../src/components/form/select/SelectContent.vue';
|
||||
import SelectItem from '../../../src/components/form/select/SelectItem.vue';
|
||||
import SelectTrigger from '../../../src/components/form/select/SelectTrigger.vue';
|
||||
import SelectValue from '../../../src/components/form/select/SelectValue.vue';
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common",
|
||||
title: 'Components/Common',
|
||||
component: SheetComponent,
|
||||
subcomponents: {
|
||||
subcomponents: {
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
SheetFooter
|
||||
SheetFooter,
|
||||
Select,
|
||||
},
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template: '<div style="min-height: 100vh;"><story /></div>',
|
||||
}),
|
||||
],
|
||||
} satisfies Meta<typeof SheetComponent>;
|
||||
|
||||
export default meta;
|
||||
@@ -82,3 +95,66 @@ export const Sheet: Story = {
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SheetWithSelect: Story = {
|
||||
render: () => ({
|
||||
components: {
|
||||
SheetComponent,
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
Button,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
theme: 'light',
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<div class="inline-flex items-center gap-4 p-4">
|
||||
<SheetComponent>
|
||||
<SheetTrigger>
|
||||
<Button variant="outline">Open Form Sheet</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right">
|
||||
<SheetHeader>
|
||||
<SheetTitle>User Preferences</SheetTitle>
|
||||
<SheetDescription>
|
||||
Configure your user preferences using the form below.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div class="py-6">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium">Theme</label>
|
||||
<Select v-model="theme">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Save changes</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</SheetComponent>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { Tooltip as TooltipComponent, TooltipTrigger, TooltipContent, TooltipProvider } from "../../../src/components/common/tooltip";
|
||||
import { Button } from "../../../src/components/common/button";
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
import { Button } from '../../../src/components/common/button';
|
||||
import {
|
||||
Tooltip as TooltipComponent,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '../../../src/components/common/tooltip';
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common",
|
||||
title: 'Components/Common',
|
||||
component: TooltipComponent,
|
||||
} satisfies Meta<typeof TooltipComponent>;
|
||||
|
||||
@@ -21,8 +26,6 @@ export const Tooltip: Story = {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div id="modals"></div>
|
||||
<div class="p-20 flex items-center justify-start">
|
||||
<TooltipProvider>
|
||||
<TooltipComponent :default-open="args.defaultOpen">
|
||||
@@ -35,7 +38,6 @@ export const Tooltip: Story = {
|
||||
</TooltipComponent>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
import {
|
||||
Select as SelectComponent,
|
||||
SelectContent,
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../../src/components/form/select";
|
||||
} from '../../../src/components/form/select';
|
||||
|
||||
const meta = {
|
||||
title: "Components/Form/Select",
|
||||
title: 'Components/Form/Select',
|
||||
component: SelectComponent,
|
||||
} satisfies Meta<typeof SelectComponent>;
|
||||
|
||||
@@ -33,8 +33,6 @@ export const Select: Story = {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div id="modals"></div>
|
||||
<SelectComponent>
|
||||
<SelectTrigger class="w-[180px]">
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
@@ -49,7 +47,6 @@ export const Select: Story = {
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</SelectComponent>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -70,7 +67,7 @@ export const Grouped: Story = {
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div id="modals"></div>
|
||||
<unraid-modals></unraid-modals>
|
||||
<SelectComponent>
|
||||
<SelectTrigger class="w-[180px]">
|
||||
<SelectValue placeholder="Select a food" />
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import tailwindRemToRem from '@unraid/tailwind-rem-to-rem';
|
||||
import type { Config } from 'tailwindcss';
|
||||
import tailwindcssAnimate from 'tailwindcss-animate';
|
||||
import { unraidPreset } from './src/theme/preset';
|
||||
|
||||
|
||||
export default {
|
||||
darkMode: ['class'],
|
||||
presets: [unraidPreset],
|
||||
content: [
|
||||
'./src/components/**/*.{js,vue,ts}',
|
||||
'./src/components/**/*.ce.{js,vue,ts}',
|
||||
'./src/composables/**/*.{js,vue,ts}',
|
||||
'./stories/**/*.stories.{js,ts,jsx,tsx,mdx}',
|
||||
'./stories/**/*.stories.{js,ts,jsx,mdx}',
|
||||
'./index.html',
|
||||
],
|
||||
safelist: [
|
||||
'dark',
|
||||
'DropdownWrapper_blip',
|
||||
'unraid_mark_1',
|
||||
'unraid_mark_2',
|
||||
'unraid_mark_3',
|
||||
@@ -37,5 +37,57 @@ export default {
|
||||
baseFontSize: 16,
|
||||
newFontSize: Number(process.env.VITE_TAILWIND_BASE_FONT_SIZE ?? 10),
|
||||
}),
|
||||
tailwindcssAnimate,
|
||||
],
|
||||
} satisfies Partial<Config>;
|
||||
theme: {
|
||||
extend: {
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Partial<Config>;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user