├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── publish.yml
├── .gitignore
├── Makefile
├── README.md
├── archive
├── docker.labelInjector-2024.07.29.txz
├── docker.labelInjector-2024.07.31.txz
├── docker.labelInjector-2024.08.26.txz
├── docker.labelInjector-2024.08.28.txz
├── docker.labelInjector-2024.09.02.txz
├── docker.labelInjector-2024.09.30.txz
├── docker.labelInjector-2024.10.02.txz
├── docker.labelInjector-2024.10.03.txz
├── docker.labelInjector-2024.10.05.txz
├── docker.labelInjector-2024.10.29.txz
└── docker.labelInjector-2024.11.03.txz
├── docker.labelInjector.plg
├── images
├── button.png
├── form.png
└── settings.png
├── pkg_build.sh
└── src
└── docker.labelInjector
└── usr
└── local
└── emhttp
└── plugins
└── docker.labelInjector
├── Readme.md
├── docker.labelInjector.Docker.page
├── docker.labelInjector.page
├── icon.png
├── scripts
├── config.js
├── docker.js
└── dropdown.js
├── server
├── config
│ └── DefaultLabels.php
└── service
│ └── AddLabels.php
└── styles
└── styles.css
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 | Please include any labels you think may be causing the to help replicate
13 |
14 | **To Reproduce**
15 | Steps to reproduce the behavior:
16 |
17 | 1. Go to '...'
18 | 2. Click on '....'
19 | 3. Scroll down to '....'
20 | 4. See error
21 |
22 | **Expected behavior**
23 | A clear and concise description of what you expected to happen.
24 |
25 | **Screenshots**
26 | If applicable, add screenshots to help explain your problem.
27 |
28 | **Unraid version**
29 |
30 | **Any logs**
31 | i.e
32 |
33 | * /var/log/phplog - for php error logs
34 | * right click and inspect check the console for js errors
35 |
36 | **Additional context**
37 | Add any other context about the problem here.
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | push:
4 | branches:
5 | - main
6 | jobs:
7 | publish:
8 | concurrency:
9 | group: publish
10 | cancel-in-progress: false
11 | permissions:
12 | id-token: write
13 | contents: write
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
17 | - name: Run pkg_build.sh
18 | run: bash pkg_build.sh
19 | - name: Commit and push changes
20 | uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
21 | with:
22 | commit_message: 'Publish plg changes'
23 | file_pattern: 'archive/* *.plg'
24 | commit_user_name: 'phyzical[bot]'
25 | commit_user_email: '5182053+phyzical@users.noreply.github.com'
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | tmp
3 | .DS_Store
4 | .vscode
5 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | -include .env
2 | DIR=/usr/local/emhttp/plugins/docker.versions
3 | SSH_HOST=${USERNAME}@${HOST}
4 |
5 |
6 | build:
7 | bash pkg_build.sh
8 |
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # docker.labelInjector
2 |
3 | Install via CA Apps
4 |
5 | You can add defaults to be prefilled each time via the settings page
6 | 
7 |
8 | After installing, just click the "Add Labels" button
9 |
10 | 
11 |
12 | Then simply choose the containers you want to add the label value combos to, to choose All for all.
13 |
14 | If the label exists already an update will be performed.
15 |
16 | If the label does not exist it will be added.
17 |
18 | If you enter a value of `REMOVE` it will instead remove the label if found.
19 |
20 | Special flags include:
21 |
22 | * will be replace with the magic value works with both key and value
23 |
24 | * `${CONTAINER_NAME}`
25 |
26 | Before it updates it will backup the template being used just incase something does go wrong.
27 | 
28 |
29 | If you find this happen you should be able to restore the backup via `/boot/config/plugins/dockerMan/templates-user/my-TEMPLATE_NAME.DATE.bak`
30 |
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.07.29.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.07.29.txz
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.07.31.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.07.31.txz
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.08.26.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.08.26.txz
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.08.28.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.08.28.txz
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.09.02.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.09.02.txz
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.09.30.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.09.30.txz
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.10.02.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.10.02.txz
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.10.03.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.10.03.txz
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.10.05.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.10.05.txz
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.10.29.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.10.29.txz
--------------------------------------------------------------------------------
/archive/docker.labelInjector-2024.11.03.txz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phyzical/docker.labelInjector/19dd8e6f1189e681f966dbacee252f8758c459b1/archive/docker.labelInjector-2024.11.03.txz
--------------------------------------------------------------------------------
/docker.labelInjector.plg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ]>
12 |
13 |
']; 46 | if (hasUpdates) { 47 | updates.push("") 59 | 60 | swal({ 61 | title: "Summary of Updates", 62 | text: updates.join(""), 63 | html: true, 64 | closeOnConfirm: false, 65 | }, function () { 66 | $(".sweet-alert").removeClass("label-injector-summary"); 67 | swal.close(); // Close the SweetAlert dialog 68 | if (hasUpdates) { 69 | $('div.spinner.fixed').show(); 70 | const containersString = data.containers.map(container => encodeURIComponent(container)); 71 | setTimeout(() => { 72 | $('div.spinner.fixed').hide(); 73 | openDocker('update_container ' + containersString.join("*"), _(`Updating ${data.containers.length} Containers`), '', 'loadlist'); 74 | }, 500); 75 | } 76 | }); 77 | $(".sweet-alert").addClass("label-injector-summary") 78 | }); 79 | } 80 | } 81 | 82 | function labelForm() { 83 | $('#label-injector-form').html(` 84 | 108 | `) 109 | generateLabelsSelect(); 110 | generateContainersSelect(); 111 | 112 | $(".sa-confirm-button-container button").prop("disabled", true) 113 | const valueChecker = function () { 114 | if ($("#label-injector-containers").val() && $("#label-injector-labels").val()) { 115 | $(".sa-confirm-button-container button").prop("disabled", false) 116 | } else { 117 | $(".sa-confirm-button-container button").prop("disabled", true) 118 | } 119 | } 120 | $("#label-injector-containers").on('change', valueChecker); 121 | $("#label-injector-labels").on('change', valueChecker); 122 | } 123 | 124 | function generateLabelsSelect() { 125 | generateDropdown("#label-injector-labels", { 126 | choices: defaultLabels.map(label => ({ 127 | value: label, 128 | label: label, 129 | selected: true, 130 | disabled: false 131 | })), 132 | addItemFilter: (value) => !!value && value !== '' && value.includes('='), 133 | customAddItemText: 'Only values containing "=" can be added, i.e `LABEL_A=VALUE_A', 134 | }, "#remove-all-label-injector-labels") 135 | } 136 | 137 | function generateContainersSelect() { 138 | generateDropdown("#label-injector-containers", { 139 | choices: docker.map(ct => ({ 140 | value: ct.name, 141 | label: ct.name, 142 | selected: false, 143 | disabled: false 144 | })).concat({ 145 | value: 'all', 146 | label: 'All', 147 | selected: false, 148 | disabled: false 149 | }), 150 | }, "#remove-all-label-injector-containers") 151 | } -------------------------------------------------------------------------------- /src/docker.labelInjector/usr/local/emhttp/plugins/docker.labelInjector/scripts/dropdown.js: -------------------------------------------------------------------------------- 1 | const defaultOptions = { 2 | silent: false, 3 | items: [], 4 | choices: [], 5 | renderChoiceLimit: -1, 6 | maxItemCount: -1, 7 | closeDropdownOnSelect: 'auto', 8 | singleModeForMultiSelect: false, 9 | addChoices: true, 10 | addItems: true, 11 | removeItems: true, 12 | removeItemButton: true, 13 | removeItemButtonAlignLeft: false, 14 | editItems: true, 15 | allowHTML: false, 16 | allowHtmlUserInput: false, 17 | duplicateItemsAllowed: true, 18 | delimiter: ',', 19 | paste: true, 20 | searchEnabled: true, 21 | searchChoices: true, 22 | searchFloor: 1, 23 | searchResultLimit: 4, 24 | searchFields: ['label', 'value'], 25 | position: 'auto', 26 | resetScrollPosition: true, 27 | shouldSort: true, 28 | shouldSortItems: false, 29 | shadowRoot: null, 30 | placeholder: true, 31 | placeholderValue: null, 32 | searchPlaceholderValue: null, 33 | prependValue: null, 34 | appendValue: null, 35 | renderSelectedChoices: 'auto', 36 | loadingText: 'Loading...', 37 | noResultsText: 'No results found', 38 | noChoicesText: 'No choices to choose from', 39 | itemSelectText: 'Press to select', 40 | uniqueItemText: 'Only unique values can be added', 41 | customAddItemText: 'Only values containing "=" can be added, i.e `LABEL_A=VALUE_A', 42 | addItemText: (value) => { 43 | return `Press Enter to add "${value}"`; 44 | }, 45 | removeItemIconText: () => `Remove item`, 46 | removeItemLabelText: (value) => `Remove item: ${value}`, 47 | maxItemText: (maxItemCount) => { 48 | return `Only ${maxItemCount} values can be added`; 49 | }, 50 | valueComparer: (value1, value2) => { 51 | return value1 === value2; 52 | }, 53 | classNames: { 54 | containerOuter: ['choices'], 55 | containerInner: ['choices__inner'], 56 | input: ['choices__input'], 57 | inputCloned: ['choices__input--cloned'], 58 | list: ['choices__list'], 59 | listItems: ['choices__list--multiple'], 60 | listSingle: ['choices__list--single'], 61 | listDropdown: ['choices__list--dropdown'], 62 | item: ['choices__item'], 63 | itemSelectable: ['choices__item--selectable'], 64 | itemDisabled: ['choices__item--disabled'], 65 | itemChoice: ['choices__item--choice'], 66 | description: ['choices__description'], 67 | placeholder: ['choices__placeholder'], 68 | group: ['choices__group'], 69 | groupHeading: ['choices__heading'], 70 | button: ['choices__button'], 71 | activeState: ['is-active'], 72 | focusState: ['is-focused'], 73 | openState: ['is-open'], 74 | disabledState: ['is-disabled'], 75 | highlightedState: ['is-highlighted'], 76 | selectedState: ['is-selected'], 77 | flippedState: ['is-flipped'], 78 | loadingState: ['is-loading'], 79 | notice: ['choices__notice'], 80 | addChoice: ['choices__item--selectable', 'add-choice'], 81 | noResults: ['has-no-results'], 82 | noChoices: ['has-no-choices'], 83 | }, 84 | // Choices uses the great Fuse library for searching. You 85 | // can find more options here: https://fusejs.io/api/options.html 86 | fuseOptions: { 87 | includeScore: true 88 | }, 89 | labelId: '', 90 | callbackOnInit: null, 91 | callbackOnCreateTemplates: null, 92 | appendGroupInSearch: false, 93 | } 94 | 95 | function generateDropdown(selector, options, removeAllSelector = undefined) { 96 | const choicesSelect = new Choices($(selector)[0], { ...defaultOptions, ...options }); 97 | 98 | if (removeAllSelector === undefined) { 99 | return choicesSelect; 100 | } 101 | $(removeAllSelector).on('click', () => { 102 | const allItems = choicesSelect.getValue(true); 103 | allItems.forEach(item => { 104 | choicesSelect.removeActiveItemsByValue(item); 105 | }); 106 | }); 107 | 108 | 109 | let selectedAll = false; 110 | $(selector).on('change', function () { 111 | if ($(this).val().includes('all')) { 112 | if (!selectedAll) { 113 | selectedAll = true 114 | const allChoices = choices._store.choices; 115 | allChoices.forEach(choice => { 116 | if (!choice.selected && !choice.disabled) { 117 | choices.setChoiceByValue(choice.value); 118 | } 119 | }); 120 | } 121 | } else { 122 | if (selectedAll) { 123 | selectedAll = false 124 | const allChoices = choices._store.choices; 125 | allChoices.forEach(choice => { 126 | if (choice.selected && !choice.disabled) { 127 | choices.removeActiveItemsByValue(choice.value); 128 | } 129 | }); 130 | } 131 | } 132 | }) 133 | } -------------------------------------------------------------------------------- /src/docker.labelInjector/usr/local/emhttp/plugins/docker.labelInjector/server/config/DefaultLabels.php: -------------------------------------------------------------------------------- 1 | $labels]); 22 | mkdir(DefaultLabels::CONFIG_PATH, 0755, true); 23 | file_put_contents(DefaultLabels::LABELS_PATH, $labelsJson); 24 | return "Labels saved successfully!"; 25 | } 26 | return null; 27 | } 28 | 29 | 30 | /** 31 | * Get the default labels from the config file. 32 | * @return string[] 33 | */ 34 | static function getDefaultLabels(): array 35 | { 36 | $json = ""; 37 | if (file_exists(self::LABELS_PATH)) { 38 | $json = file_get_contents(self::LABELS_PATH); 39 | } 40 | if (!$json || empty($json)) { 41 | return []; 42 | } 43 | 44 | return array_map(function ($item) { 45 | return str_replace('"', self::QUOTE_REPLACER, $item); 46 | }, json_decode($json)->labels); 47 | } 48 | 49 | /** 50 | * Generate the form for the default labels. 51 | */ 52 | // TODO: reuse the form select config and here 53 | static function generateForm(): void 54 | { 55 | $message = self::formSubmit(); 56 | echo <<Default Labels 58 |Note: The templates have been updated, this is just an FYI modal at the moment
") 48 | updates.push("Note: if you leave this page the label will not be applied until you edit and save the container/s in question
") 49 | updates.push("Note: Performing this action will also update the container at this time
") 50 | updates.push("Once you press okay the changes will be applied one by one
") 51 | Object.entries(data.updates).forEach(([container, changes]) => { 52 | updates.push(`${container} changes:
${changes.join("")}`); 53 | }); 54 | } else { 55 | updates.push("No Containers returned any changes in labels, nothing to be applied
") 56 | } 57 | 58 | updates.push("
Labels to be prefilled when using the add labels button
59 |Type and press enter to save a label, separate label from value via '='
60 |Empty values are valid to allow for easy filling
61 | HTML; 62 | 63 | echo "To use quotes in an options use an escaped backtick " . self::QUOTE_REPLACER . " Otherwise the option fails to save
"; 64 | echo <<The following special values are available: 67 |$message
"; 90 | } 91 | echo "Actioning {$templatePath}
"]; 47 | $old_template_xml = $template_xml->asXML(); 48 | 49 | foreach ($inputs as $input) { 50 | $label = $input->key; 51 | $value = $input->value; 52 | $label = str_replace("\${CONTAINER_NAME}", $containerName, $label); 53 | 54 | $template_label = $template_xml->xpath("//Config[@Type='Label'][@Target='$label']"); 55 | $value = str_replace("\${CONTAINER_NAME}", $containerName, $value); 56 | 57 | if ($template_label) { 58 | if (!$value) { 59 | $changes[] = "Removing $label
"; 60 | $dom = dom_import_simplexml($template_label[0]); 61 | $dom->parentNode->removeChild($dom); 62 | $changed = true; 63 | } else if ($template_label[0][0] != $value) { 64 | $changes[] = "Updating $label to $value
"; 65 | $template_label[0][0] = $value; 66 | $changed = true; 67 | } 68 | } else if ($value) { 69 | $changes[] = "Adding $label with $value
"; 70 | $newElement = $template_xml->addChild('Config'); 71 | $newElement->addAttribute('Name', $label); 72 | $newElement->addAttribute('Target', $label); 73 | $newElement->addAttribute('Default', ""); 74 | $newElement->addAttribute('Mode', ""); 75 | $newElement->addAttribute('Description', ""); 76 | $newElement->addAttribute('Type', 'Label'); 77 | $newElement->addAttribute('Display', 'always'); 78 | $newElement->addAttribute('Required', 'false'); 79 | $newElement->addAttribute('Mask', 'false'); 80 | 81 | $newElement[0] = $value; 82 | $changed = true; 83 | } 84 | } 85 | 86 | if ($changed) { 87 | // Backup Juust incase 88 | file_put_contents($templatePath . "." . (new DateTime())->format('Y.m.d.H.I.s') . ".bak", $old_template_xml); 89 | file_put_contents($templatePath, $template_xml->asXML()); 90 | $updatedContainerNames[] = $containerName; 91 | $updateSummaries[$containerName] = $changes; 92 | } 93 | } 94 | } 95 | 96 | echo json_encode(["containers" => $updatedContainerNames, "updates" => $updateSummaries]); 97 | ?> -------------------------------------------------------------------------------- /src/docker.labelInjector/usr/local/emhttp/plugins/docker.labelInjector/styles/styles.css: -------------------------------------------------------------------------------- 1 | .label-injector-form { 2 | text-align: center !important; 3 | } 4 | .label-injector-form p { 5 | text-align: center !important; 6 | } 7 | 8 | #label-injector-form { 9 | height: 75%; 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: space-between; 13 | } 14 | 15 | .sweet-alert.label-injector { 16 | width: 75% !important; 17 | left: 25%; 18 | top: 25%; 19 | } 20 | 21 | .sweet-alert.label-injector-summary { 22 | width: 75% !important; 23 | left: 25%; 24 | } 25 | 26 | .choices__button { 27 | min-width: 1px !important; 28 | width: 1px !important; 29 | } 30 | 31 | .label-injector-form-group-divider { 32 | height: 100px; 33 | } 34 | 35 | .choices__item { 36 | color: black; 37 | } 38 | 39 | #label-injector-form ul { 40 | list-style-type: none; /* Remove default list styling */ 41 | padding: 0; /* Remove default padding */ 42 | text-align: center; /* Center the text */ 43 | } 44 | 45 | #label-injector-form ul li { 46 | margin: 0 10px; /* Add some margin between items */ 47 | width: auto; /* Ensure items do not take up the maximum width */ 48 | } 49 | 50 | .docker-label-updates { 51 | overflow-y: scroll; 52 | height:400px; 53 | border: 2px solid #000; 54 | padding: 10px; 55 | border-radius: 5px; 56 | max-width: 100%; 57 | white-space: pre-wrap; 58 | word-wrap: break-word; 59 | } 60 | 61 | .docker-label-updates * { 62 | color: inherit !important; 63 | } --------------------------------------------------------------------------------