OXYGENHUB
◆ v0.9.22 oxygenhub / templates / org_settings.html
14.3 KiB · HTML · last changed Runner updates in waves: a later wave takes a version once the waves before it proved it; an error stops it (refs andymccall/oxygenflow#84) 8 days ago source blame history raw
1
{% extends "base.html" %}
2
{% import "webhooks_panel.html" as w %}
3
{% import "assistants_panel.html" as ai %}
4
{% import "variables_panel.html" as va %}
5
{% import "secrets_panel.html" as se %}
6
{% import "runner_update_fields.html" as ruf %}
7
{% block title %}{{ org.name }} settings{% endblock %}
8
{% block content %}
9
<div class="head"><h1>{{ org.name }} <small>organisation settings · <a href="/{{ org.name }}">repositories</a></small></h1></div>
10
<div class="sectioned">
11
<nav class="section-nav" aria-label="Sections">
12
  <a href="#general">General</a>
13
  <a href="#logo">Logo</a>
14
  <a href="#members">Members</a>
15
  <a href="#webhooks">Webhooks</a>
16
  <a href="#variables">Variables</a>
17
  <a href="#secrets">Secrets</a>
18
  <a href="#assistants">AI assistants</a>
19
  <a href="#teams">Teams</a>
20
  <a href="#storage">Storage</a>
21
  <a href="#backups">Backups</a>
22
</nav>
23
<div class="sections">
24
{% if let Some(n) = notice %}<p class="note">{{ n }}</p>{% endif %}
25
{% if let Some(e) = error %}<p class="error">{{ e }}</p>{% endif %}
26
<section class="panel" id="backups"><h2>Backups</h2><p class="small">Copy every repository {{ org.name }} owns, with its issues, proposals and wiki, hourly to a bucket or directory you control. <a class="go small" href="/orgs/{{ org.name }}/settings/backups">Set up backups</a></p></section>
27
<form class="card" id="general" method="post" action="/orgs/{{ org.name }}/settings">
28
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
29
  <label for="display_name">Display name</label><input type="text" id="display_name" name="display_name" value="{{ org.display_name }}">
30
  <label for="description">Description</label><input type="text" id="description" name="description" value="{{ org.description }}">
31
  <label for="default_role">Members' default role on the organisation's repositories</label>
32
  <select id="default_role" name="default_role">{% for r in ["none", "read", "triage", "write", "maintain", "admin"] %}<option value="{{ r }}"{% if org.default_role.as_str() == *r %} selected{% endif %}>{{ r }}</option>{% endfor %}</select>
33
  <button type="submit">SAVE</button>
34
</form>
35
<form class="card picture-card" id="logo" method="post" action="/orgs/{{ org.name }}/settings/avatar" enctype="multipart/form-data">
36
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
37
  <h2>Logo</h2>
38
  <div class="picture-row">
39
    <img class="avatar xl" src="/avatar/{{ org.name }}?v={{ org.avatar }}" alt="{{ org.name }} logo" width="96" height="96">
40
    <div>
41
      <label for="logo-file">Upload a logo</label>
42
      <input type="file" id="logo-file" name="picture" accept="image/png,image/jpeg,image/webp">
43
      <p class="small mute">PNG (transparency is kept), JPEG or WebP, up to 5 MiB. It is cropped to the middle square and resized to 400×400.</p>
44
      <button type="submit" name="action" value="upload">UPLOAD</button>
45
      {% if !org.avatar.is_empty() %}<button type="submit" name="action" value="remove" class="danger-btn">REMOVE</button>{% endif %}
46
    </div>
47
  </div>
48
</form>
49
<section class="panel" id="members">
50
  <h2>Members</h2>
51
  <table class="small">{% for (a, r) in members %}<tr><td><a href="/{{ a.username }}">{{ a.username }}</a></td><td>{{ r }}</td>
52
  <td><form class="tiny" method="post" action="/orgs/{{ org.name }}/settings/members"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="username" value="{{ a.username }}"><button name="remove" value="1">remove</button></form></td></tr>{% endfor %}</table>
53
  <form class="card" method="post" action="/orgs/{{ org.name }}/settings/members">
54
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
55
    <div class="row"><div><label for="username">Account</label><input type="text" id="username" name="username" required></div>
56
    <div><label for="role">Role</label><select id="role" name="role"><option value="member">member</option><option value="owner">owner</option></select></div></div>
57
    <button type="submit">ADD OR CHANGE</button>
58
  </form>
59
</section>
60
<form class="card" id="flow-priority" method="post" action="/orgs/{{ org.name }}/settings/flow-priority">
61
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
62
  <h2>OxygenFlow priority</h2>
63
  <p class="small">Runners take higher levels first; within a level, repositories take turns. A repository may set its own level, and a maintainer can expedite one run.</p>
64
  <select name="priority" aria-label="Priority">
65
    <option value="default"{% if org.flow_priority.is_empty() %} selected{% endif %}>the site's</option>
66
    {% if shell.is_admin %}<option value="high"{% if org.flow_priority == "high" %} selected{% endif %}>high</option>{% endif %}
67
    <option value="normal"{% if org.flow_priority == "normal" %} selected{% endif %}>normal</option>
68
    <option value="low"{% if org.flow_priority == "low" %} selected{% endif %}>low</option>
69
  </select>
70
  <button type="submit">Save</button>
71
</form>
72
<form class="card" id="storage" method="post" action="/orgs/{{ org.name }}/settings/storage">
73
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
74
  <h2>Storage</h2>
75
  <p class="small mute">Where the organisation's new repositories may live. Repositories that exist stay where they are.</p>
76
  <label class="check"><input type="checkbox" name="require_bucket" value="1"{% if policy.require_bucket %} checked{% endif %}> Keep every new repository in a bucket, not on this server's disk</label>
77
  <label for="allowed">Buckets allowed, one store URL prefix per line (empty: any bucket)</label>
78
  <textarea id="allowed" name="allowed" rows="3" placeholder="s3://acme-london/repos/&#10;gs://acme-eu/" spellcheck="false">{{ policy.allowed.join("\n") }}</textarea>
79
  <p class="small mute">Listing the organisation's own buckets keeps its data where those buckets are: the provider, the region, the country.</p>
80
  <button type="submit">SAVE</button>
81
</form>
82
<section class="panel" id="teams">
83
  <h2>Teams</h2>
84
  {% for (t, ms) in teams %}
85
  <p><strong>{{ t.name }}</strong>: {% for a in ms %}{{ a.username }}{% if !loop.last %}, {% endif %}{% endfor %}{% if ms.is_empty() %}<span class="mute">nobody yet</span>{% endif %}
86
  <form class="tiny" method="post" action="/orgs/{{ org.name }}/settings/teams"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="team" value="{{ t.name }}"><button name="action" value="delete">delete team</button></form></p>
87
  {% endfor %}
88
  <form class="card" method="post" action="/orgs/{{ org.name }}/settings/teams">
89
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
90
    <div class="row"><div><label for="team">Team</label><input type="text" id="team" name="team" required></div>
91
    <div><label for="tuser">Account (for add or remove)</label><input type="text" id="tuser" name="username"></div></div>
92
    <div class="choice"><button type="submit" name="action" value="create">CREATE TEAM</button> <button type="submit" name="action" value="add">ADD MEMBER</button> <button type="submit" name="action" value="remove">REMOVE MEMBER</button></div>
93
  </form>
94
</section>
95
<section class="panel" id="runners">
96
  <h2>Runners</h2>
97
  <p class="small">The OxygenFlow runners that may take this organisation's jobs: the site's, the organisation's, and those of its repositories.</p>
98
  {% if let Some(t) = runner_token %}<p class="note">Register a runner for the organisation with:</p><pre class="snippet">flow runner register {{ shell.base_url }} --token {{ t }} --name NAME</pre>{% endif %}
99
  {% if runners.is_empty() %}<p class="mute small">None yet: runs wait until a runner that serves them is added.</p>{% else %}
100
  <div class="table"><table>
101
  <tr><th>Runner</th><th>Status</th><th>Serves</th><th>Machine</th><th></th></tr>
102
  {% for (r, serves, own) in runners %}<tr><td><strong>{{ r.name }}</strong></td><td><span class="runner-status st-{{ r.status() }}">{{ r.status() }}</span></td><td class="small">{{ serves }}</td><td class="small">{% if r.arch.is_empty() %}<span class="mute">unknown</span>{% else %}{{ r.arch }} · {{ r.os }}{% endif %}</td>
103
  <td>{% if *own %}<form class="tiny" method="post" action="/orgs/{{ org.name }}/settings/runners/{{ r.id }}/remove" data-confirm="Remove {{ r.name }}? Its credential stops working."><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button class="danger-btn">remove</button></form>{% endif %}</td></tr>{% endfor %}
104
  </table></div>{% endif %}
105
  <form method="post" action="/orgs/{{ org.name }}/settings/runners/token"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit">Add a runner for the organisation</button></form>
106
</section>
107
<section class="panel" id="runner-updates">
108
  <h2>Runner updates</h2>
109
  <p class="small">The organisation's own runners can update themselves: off unless chosen. Try a build on a test group first (runners you pick, or a percentage of them, the same machines every time), then turn updates on for the rest. A runner's own setting wins over a group's, a group's over the organisation's, the organisation's over the site's.</p>
110
  {% let ou = updates.owners[0] %}
111
  <form class="pol-form" method="post" action="/orgs/{{ org.name }}/settings/runners/updates/policy">
112
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="scope" value="owner"><input type="hidden" name="key" value="{{ org.name }}">
113
    <strong>The organisation</strong>{% call ruf::policy_fields(ou, false) %}<button type="submit">Save</button>
114
  </form>
115
  <h3>Test groups</h3>
116
  {% if updates.groups.is_empty() %}<p class="mute small">None yet.</p>{% endif %}
117
  {% for gv in updates.groups %}{% let g = gv.g %}
118
  <div class="card wide update-group">
119
    <h3>{{ g.name }} <small class="mute">{% if let Some(p) = g.percent %}{{ p }}%{% else %}chosen{% endif %} · {{ gv.members.len()|plural("runner", "runners") }} now{% if let Some(w) = g.wave %} · wave {{ w }}{% endif %}</small></h3>
120
    <p class="small">{% for m in gv.members %}<span class="lbl">{{ m }}</span> {% endfor %}</p>
121
    <form class="pol-form" method="post" action="/orgs/{{ org.name }}/settings/runners/updates/policy">
122
      <input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="scope" value="group"><input type="hidden" name="key" value="{{ g.id }}">
123
      {% call ruf::policy_fields(gv.policy, false) %}<button type="submit">Save settings</button>
124
    </form>
125
    <details><summary class="small">Who is in it</summary>
126
    <form method="post" action="/orgs/{{ org.name }}/settings/runners/updates/groups/{{ g.id }}">
127
      <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
128
      <label class="pol"><input type="radio" name="mode" value="percent"{% if g.percent.is_some() %} checked{% endif %}> a percentage: <input type="number" name="percent" min="1" max="100" value="{% if let Some(p) = g.percent %}{{ p }}{% endif %}">%</label>
129
      <label class="pol"><input type="radio" name="mode" value="chosen"{% if g.percent.is_none() %} checked{% endif %}> these runners:</label>
130
      <div class="pol-row">{% for rv in updates.runners %}<label class="pol"><input type="checkbox" name="runner" value="{{ rv.r.id }}"{% if gv.chosen.contains(rv.r.id) %} checked{% endif %}> {{ rv.r.name }}</label>{% endfor %}</div>
131
      <label class="pol">Wave <input type="number" name="wave" min="1" max="9" size="3" value="{% if let Some(w) = g.wave %}{{ w }}{% endif %}" title="Blank: after every wave"></label>
132
      <button type="submit">Save members</button>
133
    </form>
134
    <form class="tiny" method="post" action="/orgs/{{ org.name }}/settings/runners/updates/groups/{{ g.id }}/delete" data-confirm="Remove the test group {{ g.name }}?"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button class="danger-btn">remove group</button></form>
135
    </details>
136
  </div>
137
  {% endfor %}
138
  <form class="card wide" method="post" action="/orgs/{{ org.name }}/settings/runners/updates/groups">
139
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
140
    <h3>New test group</h3>
141
    <label class="pol">Name <input type="text" name="name" size="14" required placeholder="canary"></label>
142
    <label class="pol"><input type="radio" name="mode" value="percent"> a percentage of the organisation's runners: <input type="number" name="percent" min="1" max="100">%</label>
143
    <label class="pol"><input type="radio" name="mode" value="chosen" checked> these runners:</label>
144
    <div class="pol-row">{% for rv in updates.runners %}<label class="pol"><input type="checkbox" name="runner" value="{{ rv.r.id }}"> {{ rv.r.name }}</label>{% endfor %}</div>
145
    <label class="pol">Wave <input type="number" name="wave" min="1" max="9" size="3" title="Blank: after every wave"></label>
146
    <button type="submit">Make group</button>
147
  </form>
148
  {% if !updates.runners.is_empty() %}
149
  <h3>The organisation's runners</h3>
150
  <div class="table"><table>
151
  <tr><th>Runner</th><th>Runs</th><th>Its policy</th><th>Now</th><th>Last said</th><th>Its own settings</th></tr>
152
  {% for rv in updates.runners %}{% let r = rv.r %}
153
  <tr><td><strong>{{ r.name }}</strong>{% for g in rv.groups %} <span class="lbl">{{ g }}</span>{% endfor %}</td>
154
    <td class="small">{% if r.version.is_empty() %}<span class="mute">unknown</span>{% else %}flow {{ r.version }}{% endif %}</td>
155
    <td class="small">{{ rv.effective_words() }}</td><td class="small">{{ rv.offer }}</td>
156
    <td class="small">{% if r.update_note.is_empty() %}<span class="mute">nothing yet</span>{% else %}{{ r.update_note }}{% endif %}</td>
157
    <td><form class="pol-form tiny" method="post" action="/orgs/{{ org.name }}/settings/runners/updates/policy"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="scope" value="runner"><input type="hidden" name="key" value="{{ r.id }}">{% call ruf::policy_fields(rv.policy, false) %}<button type="submit">set</button></form></td></tr>
158
  {% endfor %}
159
  </table></div>
160
  {% endif %}
161
</section>
162
{% call w::panel("/orgs/" ~ org.name ~ "/settings", hooks, shell.csrf, "These hear every repository of the organisation, as well as each repository's own webhooks.") %}
163
{% call va::panel("/orgs/" ~ org.name ~ "/settings", variables, shell.csrf, "Jobs of every repository of the organisation get these, beside the site's and each repository's own.") %}
164
{% call se::panel("/orgs/" ~ org.name ~ "/settings", secrets, shell.csrf, "Jobs of every repository of the organisation may use these, beside the site's and each repository's own.") %}
165
{% call ai::panel("/orgs/" ~ org.name ~ "/settings", assistants, shell.csrf, "Every repository of the organisation may ask these, beside the site's and its own.") %}
166
</div>
167
</div>
168
{% endblock %}