ui_extensions.py 24.5 KB
Newer Older
1 2 3 4
import json
import os.path
import sys
import time
5
from datetime import datetime
6 7 8 9 10 11
import traceback

import git

import gradio as gr
import html
12 13
import shutil
import errno
14

15 16
from modules import extensions, shared, paths, config_states
from modules.paths_internal import config_states_dir
17
from modules.call_queue import wrap_gradio_gpu_call
18

A
AUTOMATIC 已提交
19
available_extensions = {"extensions": []}
20
STYLE_PRIMARY = ' style="color: var(--primary-400)"'
A
AUTOMATIC 已提交
21 22


23
def check_access():
J
jcowens 已提交
24
    assert not shared.cmd_opts.disable_extension_access, "extension access disabled because of command line flags"
25 26


27
def apply_and_restart(disable_list, update_list, disable_all):
28 29
    check_access()

30 31 32 33 34 35
    disabled = json.loads(disable_list)
    assert type(disabled) == list, f"wrong disable_list data for apply_and_restart: {disable_list}"

    update = json.loads(update_list)
    assert type(update) == list, f"wrong update_list data for apply_and_restart: {update_list}"

36 37 38
    if update:
        save_config_state("Backup (pre-update)")

39 40 41 42 43 44 45
    update = set(update)

    for ext in extensions.extensions:
        if ext.name not in update:
            continue

        try:
46
            ext.fetch_and_reset_hard()
47
        except Exception:
48
            print(f"Error getting updates for {ext.name}:", file=sys.stderr)
49 50 51
            print(traceback.format_exc(), file=sys.stderr)

    shared.opts.disabled_extensions = disabled
52
    shared.opts.disable_all_extensions = disable_all
53 54 55 56 57 58
    shared.opts.save(shared.config_filename)

    shared.state.interrupt()
    shared.state.need_restart = True


59 60 61 62 63
def save_config_state(name):
    current_config_state = config_states.get_config()
    if not name:
        name = "Config"
    current_config_state["name"] = name
64 65
    timestamp = datetime.now().strftime('%Y_%m_%d-%H_%M_%S')
    filename = os.path.join(config_states_dir, f"{timestamp}_{name}.json")
66 67 68 69 70 71
    print(f"Saving backup of webui/extension state to {filename}.")
    with open(filename, "w", encoding="utf-8") as f:
        json.dump(current_config_state, f)
    config_states.list_config_states()
    new_value = next(iter(config_states.all_config_states.keys()), "Current")
    new_choices = ["Current"] + list(config_states.all_config_states.keys())
72
    return gr.Dropdown.update(value=new_value, choices=new_choices), f"<span>Saved current webui/extension state to \"{filename}\"</span>"
73 74 75 76 77 78 79 80 81 82 83 84


def restore_config_state(confirmed, config_state_name, restore_type):
    if config_state_name == "Current":
        return "<span>Select a config to restore from.</span>"
    if not confirmed:
        return "<span>Cancelled.</span>"

    check_access()

    config_state = config_states.all_config_states[config_state_name]

85
    print(f"*** Restoring webui state from backup: {restore_type} ***")
86 87

    if restore_type == "extensions" or restore_type == "both":
88
        shared.opts.restore_config_state_file = config_state["filepath"]
89 90 91 92 93 94 95 96 97 98 99
        shared.opts.save(shared.config_filename)

    if restore_type == "webui" or restore_type == "both":
        config_states.restore_webui_config(config_state)

    shared.state.interrupt()
    shared.state.need_restart = True

    return ""


100
def check_updates(id_task, disable_list):
101 102
    check_access()

103 104 105 106 107 108 109 110
    disabled = json.loads(disable_list)
    assert type(disabled) == list, f"wrong disable_list data for apply_and_restart: {disable_list}"

    exts = [ext for ext in extensions.extensions if ext.remote is not None and ext.name not in disabled]
    shared.state.job_count = len(exts)

    for ext in exts:
        shared.state.textinfo = ext.name
111 112 113

        try:
            ext.check_updates()
114 115 116
        except FileNotFoundError as e:
            if 'FETCH_HEAD' not in str(e):
                raise
117 118 119 120
        except Exception:
            print(f"Error checking updates for {ext.name}:", file=sys.stderr)
            print(traceback.format_exc(), file=sys.stderr)

121 122 123
        shared.state.nextjob()

    return extension_table(), ""
124 125


126 127 128 129 130 131 132 133 134 135
def make_commit_link(commit_hash, remote, text=None):
    if text is None:
        text = commit_hash[:8]
    if remote.startswith("https://github.com/"):
        href = os.path.join(remote, "commit", commit_hash)
        return f'<a href="{href}" target="_blank">{text}</a>'
    else:
        return text


136 137 138 139 140 141 142
def extension_table():
    code = f"""<!-- {time.time()} -->
    <table id="extensions">
        <thead>
            <tr>
                <th><abbr title="Use checkbox to enable the extension; it will be enabled or disabled when you click apply button">Extension</abbr></th>
                <th>URL</th>
143
                <th><abbr title="Extension version">Version</abbr></th>
144 145 146 147 148 149 150
                <th><abbr title="Use checkbox to mark the extension for update; it will be updated when you click apply button">Update</abbr></th>
            </tr>
        </thead>
        <tbody>
    """

    for ext in extensions.extensions:
151 152
        ext.read_info_from_repo()

153
        remote = f"""<a href="{html.escape(ext.remote or '')}" target="_blank">{html.escape("built-in" if ext.is_builtin else ext.remote or '')}</a>"""
A
AUTOMATIC 已提交
154

155 156 157 158 159
        if ext.can_update:
            ext_status = f"""<label><input class="gr-check-radio gr-checkbox" name="update_{html.escape(ext.name)}" checked="checked" type="checkbox">{html.escape(ext.status)}</label>"""
        else:
            ext_status = ext.status

160 161
        style = ""
        if shared.opts.disable_all_extensions == "extra" and not ext.is_builtin or shared.opts.disable_all_extensions == "all":
162
            style = STYLE_PRIMARY
163

164 165 166
        version_link = ext.version
        if ext.commit_hash and ext.remote:
            version_link = make_commit_link(ext.commit_hash, ext.remote, ext.version)
167

168 169
        code += f"""
            <tr>
170
                <td><label{style}><input class="gr-check-radio gr-checkbox" name="enable_{html.escape(ext.name)}" type="checkbox" {'checked="checked"' if ext.enabled else ''}>{html.escape(ext.name)}</label></td>
A
AUTOMATIC 已提交
171
                <td>{remote}</td>
172
                <td>{version_link}</td>
173 174 175 176 177 178 179 180 181 182 183 184
                <td{' class="extension_status"' if ext.remote is not None else ''}>{ext_status}</td>
            </tr>
    """

    code += """
        </tbody>
    </table>
    """

    return code


185 186 187 188 189 190 191 192
def update_config_states_table(state_name):
    if state_name == "Current":
        config_state = config_states.get_config()
    else:
        config_state = config_states.all_config_states[state_name]

    config_name = config_state.get("name", "Config")
    created_date = time.asctime(time.gmtime(config_state["created_at"]))
193
    filepath = config_state.get("filepath", "<unknown>")
194 195 196 197 198

    code = f"""<!-- {time.time()} -->"""

    webui_remote = config_state["webui"]["remote"] or ""
    webui_branch = config_state["webui"]["branch"]
199
    webui_commit_hash = config_state["webui"]["commit_hash"] or "<unknown>"
200 201 202 203 204 205
    webui_commit_date = config_state["webui"]["commit_date"]
    if webui_commit_date:
        webui_commit_date = time.asctime(time.gmtime(webui_commit_date))
    else:
        webui_commit_date = "<unknown>"

S
space-nuko 已提交
206 207 208 209
    remote = f"""<a href="{html.escape(webui_remote)}" target="_blank">{html.escape(webui_remote or '')}</a>"""
    commit_link = make_commit_link(webui_commit_hash, webui_remote)
    date_link = make_commit_link(webui_commit_hash, webui_remote, webui_commit_date)

210 211 212 213 214 215 216 217 218 219 220 221
    current_webui = config_states.get_webui_config()

    style_remote = ""
    style_branch = ""
    style_commit = ""
    if current_webui["remote"] != webui_remote:
        style_remote = STYLE_PRIMARY
    if current_webui["branch"] != webui_branch:
        style_branch = STYLE_PRIMARY
    if current_webui["commit_hash"] != webui_commit_hash:
        style_commit = STYLE_PRIMARY

222
    code += f"""<h2>Config Backup: {config_name}</h2>
S
space-nuko 已提交
223 224
      <div><b>Filepath:</b> {filepath}</div>
      <div><b>Created at:</b> {created_date}</div>"""
225 226 227 228 229 230 231 232 233 234 235 236 237

    code += f"""<h2>WebUI State</h2>
      <table id="config_state_webui">
        <thead>
            <tr>
                <th>URL</th>
                <th>Branch</th>
                <th>Commit</th>
                <th>Date</th>
            </tr>
        </thead>
        <tbody>
            <tr>
S
space-nuko 已提交
238
                <td><label{style_remote}>{remote}</label></td>
239
                <td><label{style_branch}>{webui_branch}</label></td>
240 241
                <td><label{style_commit}>{commit_link}</label></td>
                <td><label{style_commit}>{date_link}</label></td>
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
            </tr>
        </tbody>
      </table>
    """

    code += """<h2>Extension State</h2>
      <table id="config_state_extensions">
        <thead>
            <tr>
                <th>Extension</th>
                <th>URL</th>
                <th>Branch</th>
                <th>Commit</th>
                <th>Date</th>
            </tr>
        </thead>
        <tbody>
    """

    ext_map = {ext.name: ext for ext in extensions.extensions}

    for ext_name, ext_conf in config_state["extensions"].items():
        ext_remote = ext_conf["remote"] or ""
        ext_branch = ext_conf["branch"] or "<unknown>"
        ext_enabled = ext_conf["enabled"]
        ext_commit_hash = ext_conf["commit_hash"] or "<unknown>"
        ext_commit_date = ext_conf["commit_date"]
        if ext_commit_date:
            ext_commit_date = time.asctime(time.gmtime(ext_commit_date))
        else:
            ext_commit_date = "<unknown>"

        remote = f"""<a href="{html.escape(ext_remote)}" target="_blank">{html.escape(ext_remote or '')}</a>"""
S
space-nuko 已提交
275 276
        commit_link = make_commit_link(ext_commit_hash, ext_remote)
        date_link = make_commit_link(ext_commit_hash, ext_remote, ext_commit_date)
277 278 279 280 281 282 283 284 285

        style_enabled = ""
        style_remote = ""
        style_branch = ""
        style_commit = ""
        if ext_name in ext_map:
            current_ext = ext_map[ext_name]
            current_ext.read_info_from_repo()
            if current_ext.enabled != ext_enabled:
286
                style_enabled = STYLE_PRIMARY
287
            if current_ext.remote != ext_remote:
288
                style_remote = STYLE_PRIMARY
289
            if current_ext.branch != ext_branch:
290
                style_branch = STYLE_PRIMARY
291
            if current_ext.commit_hash != ext_commit_hash:
292
                style_commit = STYLE_PRIMARY
293 294 295 296 297 298

        code += f"""
            <tr>
                <td><label{style_enabled}><input class="gr-check-radio gr-checkbox" type="checkbox" disabled="true" {'checked="checked"' if ext_enabled else ''}>{html.escape(ext_name)}</label></td>
                <td><label{style_remote}>{remote}</label></td>
                <td><label{style_branch}>{ext_branch}</label></td>
299 300
                <td><label{style_commit}>{commit_link}</label></td>
                <td><label{style_commit}>{date_link}</label></td>
301 302 303 304 305 306 307 308 309 310 311
            </tr>
    """

    code += """
        </tbody>
    </table>
    """

    return code


A
AUTOMATIC 已提交
312 313 314 315 316 317 318 319
def normalize_git_url(url):
    if url is None:
        return ""

    url = url.replace(".git", "")
    return url


320
def install_extension_from_url(dirname, url, branch_name=None):
321 322
    check_access()

323 324 325 326
    assert url, 'No URL specified'

    if dirname is None or dirname == "":
        *parts, last_part = url.split('/')
A
AUTOMATIC 已提交
327
        last_part = normalize_git_url(last_part)
328 329 330 331 332 333

        dirname = last_part

    target_dir = os.path.join(extensions.extensions_dir, dirname)
    assert not os.path.exists(target_dir), f'Extension directory already exists: {target_dir}'

A
AUTOMATIC 已提交
334 335
    normalized_url = normalize_git_url(url)
    assert len([x for x in extensions.extensions if normalize_git_url(x.remote) == normalized_url]) == 0, 'Extension with this URL is already installed'
336

337
    tmpdir = os.path.join(paths.data_path, "tmp", dirname)
338 339 340

    try:
        shutil.rmtree(tmpdir, True)
341
        if not branch_name:
Y
yike5460 已提交
342 343 344 345 346 347 348 349 350 351
            # if no branch is specified, use the default branch
            with git.Repo.clone_from(url, tmpdir) as repo:
                repo.remote().fetch()
                for submodule in repo.submodules:
                    submodule.update()
        else:
            with git.Repo.clone_from(url, tmpdir, branch=branch_name) as repo:
                repo.remote().fetch()
                for submodule in repo.submodules:
                    submodule.update()
352 353 354 355 356 357 358 359 360
        try:
            os.rename(tmpdir, target_dir)
        except OSError as err:
            if err.errno == errno.EXDEV:
                # Cross device link, typical in docker or when tmp/ and extensions/ are on different file systems
                # Since we can't use a rename, do the slower but more versitile shutil.move()
                shutil.move(tmpdir, target_dir)
            else:
                # Something else, not enough free space, permissions, etc.  rethrow it so that it gets handled.
F
Ftps 已提交
361
                raise err
362

363 364 365
        import launch
        launch.run_extension_installer(target_dir)

366 367 368 369 370 371
        extensions.list_extensions()
        return [extension_table(), html.escape(f"Installed into {target_dir}. Use Installed tab to restart.")]
    finally:
        shutil.rmtree(tmpdir, True)


372
def install_extension_from_index(url, hide_tags, sort_column, filter_text):
A
AUTOMATIC 已提交
373 374
    ext_table, message = install_extension_from_url(None, url)

375
    code, _ = refresh_available_extensions_from_data(hide_tags, sort_column, filter_text)
A
AUTOMATIC 已提交
376

377
    return code, ext_table, message, ''
A
AUTOMATIC 已提交
378

379

380
def refresh_available_extensions(url, hide_tags, sort_column):
A
AUTOMATIC 已提交
381 382 383 384 385 386 387 388
    global available_extensions

    import urllib.request
    with urllib.request.urlopen(url) as response:
        text = response.read()

    available_extensions = json.loads(text)

389
    code, tags = refresh_available_extensions_from_data(hide_tags, sort_column)
390

391
    return url, code, gr.CheckboxGroup.update(choices=tags), '', ''
392 393


394 395 396 397 398 399 400 401
def refresh_available_extensions_for_tags(hide_tags, sort_column, filter_text):
    code, _ = refresh_available_extensions_from_data(hide_tags, sort_column, filter_text)

    return code, ''


def search_extensions(filter_text, hide_tags, sort_column):
    code, _ = refresh_available_extensions_from_data(hide_tags, sort_column, filter_text)
A
AUTOMATIC 已提交
402

403
    return code, ''
A
AUTOMATIC 已提交
404

405

406 407 408 409 410 411 412 413 414 415
sort_ordering = [
    # (reverse, order_by_function)
    (True, lambda x: x.get('added', 'z')),
    (False, lambda x: x.get('added', 'z')),
    (False, lambda x: x.get('name', 'z')),
    (True, lambda x: x.get('name', 'z')),
    (False, lambda x: 'z'),
]


416
def refresh_available_extensions_from_data(hide_tags, sort_column, filter_text=""):
A
AUTOMATIC 已提交
417 418 419
    extlist = available_extensions["extensions"]
    installed_extension_urls = {normalize_git_url(extension.remote): extension.name for extension in extensions.extensions}

420 421 422 423
    tags = available_extensions.get("tags", {})
    tags_to_hide = set(hide_tags)
    hidden = 0

A
AUTOMATIC 已提交
424 425 426 427 428 429 430 431 432 433 434 435
    code = f"""<!-- {time.time()} -->
    <table id="available_extensions">
        <thead>
            <tr>
                <th>Extension</th>
                <th>Description</th>
                <th>Action</th>
            </tr>
        </thead>
        <tbody>
    """

436 437 438
    sort_reverse, sort_function = sort_ordering[sort_column if 0 <= sort_column < len(sort_ordering) else 0]

    for ext in sorted(extlist, key=sort_function, reverse=sort_reverse):
A
AUTOMATIC 已提交
439
        name = ext.get("name", "noname")
440
        added = ext.get('added', 'unknown')
A
AUTOMATIC 已提交
441 442
        url = ext.get("url", None)
        description = ext.get("description", "")
443
        extension_tags = ext.get("tags", [])
A
AUTOMATIC 已提交
444 445 446 447

        if url is None:
            continue

A
AUTOMATIC 已提交
448 449 450
        existing = installed_extension_urls.get(normalize_git_url(url), None)
        extension_tags = extension_tags + ["installed"] if existing else extension_tags

451 452 453 454
        if len([x for x in extension_tags if x in tags_to_hide]) > 0:
            hidden += 1
            continue

455 456 457 458 459
        if filter_text and filter_text.strip():
            if filter_text.lower() not in html.escape(name).lower() and filter_text.lower() not in html.escape(description).lower():
                hidden += 1
                continue

A
AUTOMATIC 已提交
460
        install_code = f"""<button onclick="install_extension_from_index(this, '{html.escape(url)}')" {"disabled=disabled" if existing else ""} class="lg secondary gradio-button custom-button">{"Install" if not existing else "Installed"}</button>"""
A
AUTOMATIC 已提交
461

462 463
        tags_text = ", ".join([f"<span class='extension-tag' title='{tags.get(x, '')}'>{x}</span>" for x in extension_tags])

A
AUTOMATIC 已提交
464 465
        code += f"""
            <tr>
466
                <td><a href="{html.escape(url)}" target="_blank">{html.escape(name)}</a><br />{tags_text}</td>
467
                <td>{html.escape(description)}<p class="info"><span class="date_added">Added: {html.escape(added)}</span></p></td>
A
AUTOMATIC 已提交
468 469
                <td>{install_code}</td>
            </tr>
A
AUTOMATIC 已提交
470 471 472 473 474
        
        """

        for tag in [x for x in extension_tags if x not in tags]:
            tags[tag] = tag
A
AUTOMATIC 已提交
475 476 477 478 479 480

    code += """
        </tbody>
    </table>
    """

481 482 483 484
    if hidden > 0:
        code += f"<p>Extension hidden: {hidden}</p>"

    return code, list(tags)
A
AUTOMATIC 已提交
485 486


487 488 489
def create_ui():
    import modules.ui

490 491
    config_states.list_config_states()

492 493
    with gr.Blocks(analytics_enabled=False) as ui:
        with gr.Tabs(elem_id="tabs_extensions") as tabs:
494
            with gr.TabItem("Installed", id="installed"):
495

496
                with gr.Row(elem_id="extensions_installed_top"):
497 498
                    apply = gr.Button(value="Apply and restart UI", variant="primary")
                    check = gr.Button(value="Check for updates")
499
                    extensions_disable_all = gr.Radio(label="Disable all extensions", choices=["none", "extra", "all"], value=shared.opts.disable_all_extensions, elem_id="extensions_disable_all")
A
AUTOMATIC 已提交
500 501
                    extensions_disabled_list = gr.Text(elem_id="extensions_disabled_list", visible=False).style(container=False)
                    extensions_update_list = gr.Text(elem_id="extensions_update_list", visible=False).style(container=False)
502

503 504 505 506 507 508 509 510
                html = ""
                if shared.opts.disable_all_extensions != "none":
                    html = """
<span style="color: var(--primary-400);">
    "Disable all extensions" was set, change it to "none" to load all extensions again
</span>
                    """
                info = gr.HTML(html)
511 512 513 514 515
                extensions_table = gr.HTML(lambda: extension_table())

                apply.click(
                    fn=apply_and_restart,
                    _js="extensions_apply",
516
                    inputs=[extensions_disabled_list, extensions_update_list, extensions_disable_all],
517 518 519 520
                    outputs=[],
                )

                check.click(
521
                    fn=wrap_gradio_gpu_call(check_updates, extra_outputs=[gr.update()]),
522
                    _js="extensions_check",
523 524
                    inputs=[info, extensions_disabled_list],
                    outputs=[extensions_table, info],
525 526
                )

527
            with gr.TabItem("Available", id="available"):
A
AUTOMATIC 已提交
528 529
                with gr.Row():
                    refresh_available_extensions_button = gr.Button(value="Load from:", variant="primary")
530
                    available_extensions_index = gr.Text(value="https://raw.githubusercontent.com/AUTOMATIC1111/stable-diffusion-webui-extensions/master/index.json", label="Extension index URL").style(container=False)
A
AUTOMATIC 已提交
531 532 533
                    extension_to_install = gr.Text(elem_id="extension_to_install", visible=False)
                    install_extension_button = gr.Button(elem_id="install_extension_button", visible=False)

534
                with gr.Row():
A
AUTOMATIC 已提交
535
                    hide_tags = gr.CheckboxGroup(value=["ads", "localization", "installed"], label="Hide extensions with tags", choices=["script", "ads", "localization", "installed"])
536
                    sort_column = gr.Radio(value="newest first", label="Order", choices=["newest first", "oldest first", "a-z", "z-a", "internal order", ], type="index")
537

538 539 540
                with gr.Row(): 
                    search_extensions_text = gr.Text(label="Search").style(container=False)
                   
A
AUTOMATIC 已提交
541 542 543 544
                install_result = gr.HTML()
                available_extensions_table = gr.HTML()

                refresh_available_extensions_button.click(
545
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions, extra_outputs=[gr.update(), gr.update(), gr.update()]),
546
                    inputs=[available_extensions_index, hide_tags, sort_column],
547
                    outputs=[available_extensions_index, available_extensions_table, hide_tags, install_result, search_extensions_text],
A
AUTOMATIC 已提交
548 549 550 551
                )

                install_extension_button.click(
                    fn=modules.ui.wrap_gradio_call(install_extension_from_index, extra_outputs=[gr.update(), gr.update()]),
552
                    inputs=[extension_to_install, hide_tags, sort_column, search_extensions_text],
A
AUTOMATIC 已提交
553 554 555
                    outputs=[available_extensions_table, extensions_table, install_result],
                )

556 557 558 559 560 561
                search_extensions_text.change(
                    fn=modules.ui.wrap_gradio_call(search_extensions, extra_outputs=[gr.update()]),
                    inputs=[search_extensions_text, hide_tags, sort_column],
                    outputs=[available_extensions_table, install_result],
                )

562 563
                hide_tags.change(
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]),
564
                    inputs=[hide_tags, sort_column, search_extensions_text],
565 566 567 568 569
                    outputs=[available_extensions_table, install_result]
                )

                sort_column.change(
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]),
570
                    inputs=[hide_tags, sort_column, search_extensions_text],
571 572 573
                    outputs=[available_extensions_table, install_result]
                )

574
            with gr.TabItem("Install from URL", id="install_from_url"):
575
                install_url = gr.Text(label="URL for extension's git repository")
Y
yike5460 已提交
576
                install_branch = gr.Text(label="Specific branch name", placeholder="Leave empty for default main branch")
577
                install_dirname = gr.Text(label="Local directory name", placeholder="Leave empty for auto")
A
AUTOMATIC 已提交
578 579
                install_button = gr.Button(value="Install", variant="primary")
                install_result = gr.HTML(elem_id="extension_install_result")
580

A
AUTOMATIC 已提交
581
                install_button.click(
582
                    fn=modules.ui.wrap_gradio_call(install_extension_from_url, extra_outputs=[gr.update()]),
583
                    inputs=[install_dirname, install_url, install_branch],
A
AUTOMATIC 已提交
584
                    outputs=[extensions_table, install_result],
585 586
                )

587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610
            with gr.TabItem("Backup/Restore"):
                with gr.Row(elem_id="extensions_backup_top_row"):
                    config_states_list = gr.Dropdown(label="Saved Configs", elem_id="extension_backup_saved_configs", value="Current", choices=["Current"] + list(config_states.all_config_states.keys()))
                    modules.ui.create_refresh_button(config_states_list, config_states.list_config_states, lambda: {"choices": ["Current"] + list(config_states.all_config_states.keys())}, "refresh_config_states")
                    config_restore_type = gr.Radio(label="State to restore", choices=["extensions", "webui", "both"], value="extensions", elem_id="extension_backup_restore_type")
                    config_restore_button = gr.Button(value="Restore Selected Config", variant="primary", elem_id="extension_backup_restore")
                with gr.Row(elem_id="extensions_backup_top_row2"):
                    config_save_name = gr.Textbox("", placeholder="Config Name", show_label=False)
                    config_save_button = gr.Button(value="Save Current Config")

                config_states_info = gr.HTML("")
                config_states_table = gr.HTML(lambda: update_config_states_table("Current"))

                config_save_button.click(fn=save_config_state, inputs=[config_save_name], outputs=[config_states_list, config_states_info])

                dummy_component = gr.Label(visible=False)
                config_restore_button.click(fn=restore_config_state, _js="config_state_confirm_restore", inputs=[dummy_component, config_states_list, config_restore_type], outputs=[config_states_info])

                config_states_list.change(
                    fn=update_config_states_table,
                    inputs=[config_states_list],
                    outputs=[config_states_table],
                )

611
    return ui