Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): add button to delete clients #12

Merged
merged 12 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
953 changes: 534 additions & 419 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/zulip_write_only_proxy/_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def logger_name_callsite(logger, method_name, event_dict):
if not event_dict.get("logger_name"):
logger_name = f"{event_dict.pop('module')}.{event_dict.pop('func_name')}"
if not event_dict.pop("disable_name", False):
event_dict["logger_name"] = logger_name.strip(".")
event_dict["logger_name"] = logger_name.strip(".") # pyright: ignore[reportInvalidTypeForm]

return event_dict

Expand Down
3 changes: 3 additions & 0 deletions src/zulip_write_only_proxy/frontend/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
{% block content %}
{% endblock %}
</div>

<div id="toasts" class="toast toast-end"></div>

<script>
document.body.addEventListener("htmx:beforeOnLoad", function (evt) {
if ([403, 401].includes(evt.detail.xhr.status)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
<tr class="hover" key="{{ client.token.get_secret_value() }}">
<td>
{{ client.proposal_no }}
{% if client.bot_site.host != "mylog.connect.xfel.eu" %}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="h-6 w-6"
>
<path
fill-rule="evenodd"
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clip-rule="evenodd"
/>
</svg>
{% endif %}
{% if client.bot_site.host != "mylog.connect.xfel.eu" %}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="h-6 w-6"
>
<path
fill-rule="evenodd"
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clip-rule="evenodd"
/>
</svg>
{% endif %}
</td>
<td>{{ client.created_at.strftime("%Y-%m-%d %H:%M") }}</td>
<td>{{ client.created_by.replace("@xfel.eu", "") }}</td>
Expand Down Expand Up @@ -46,6 +46,9 @@
hx-get="{{ url_for('client_messages') }}"
hx-on::before-request="viewMessagesKey = getKeyForRow(event);"
hx-headers="js:{'X-API-key': getKeyForRow(event)}"
{% if client.bot_site.host != "mylog.connect.xfel.eu" %}
style="background-color: transparent;" disabled
{% endif %}
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -60,35 +63,50 @@
/>
</svg>
</button>
{% if False %}
<div class="dropdown dropdown-right">
<div tabindex="0" role="button" class=" btn btn-ghost">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="h-6 w-6"
>
<path
fill-rule="evenodd"
d="M4.5 12a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Zm6 0a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Zm6 0a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Z"
clip-rule="evenodd"
/>
</svg>
</div>
<ul
tabindex="0"
class="menu dropdown-content z-[1] w-52 rounded-box bg-base-100 p-2 shadow"
<div class="dropdown dropdown-left">
<div tabindex="0" role="button" class="btn btn-ghost">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<li><a>Test</a></li>
<li><a>Info</a></li>
<li><a>Delete</a></li>
</ul>
<div />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
/>
</svg>
</div>
{% endif %}
<ul
tabindex="0"
class="menu dropdown-content z-[1] w-52 rounded-box bg-base-100 p-2 shadow"
>
<li>
<a
class="pointer-events-none opacity-60"
style="cursor: not-allowed"
disabled
>
Info
</a>
</li>
<li>
<a class="hover:bg-error" onclick="deleteClient(this)">
Delete
<div
id="spinner"
class="text loading loading-spinner hidden"
></div>
</a>
</li>
</ul>
<div />
</div>
</td>
</td>
</tr>
{% endfor %}

<script>
Expand All @@ -108,5 +126,82 @@
});
}

function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function sendToast(msg, type) {
let toast = document.createElement("div");

toast.innerHTML = `
<div class="alert ${type}">
<span>${msg}</span>
</div>
`;

document.getElementById("toasts").appendChild(toast);

await wait(5000);

toast.remove();
}

async function deleteClient(element) {
let client = element.closest("tr");
let key = client.getAttribute("key");
let proposal_no = client.getElementsByTagName("td")[0].innerText;

if (
!confirm(`Are you sure you want to delete the client for ${proposal_no}?`)
) {
return;
}

let toast = document.createElement("div");

let spinner = element.getElementsByTagName("div")[0];
spinner.classList.remove("hidden");

try {
const response = await fetch("{{ url_for('client_delete') }}", {
method: "DELETE",
headers: {
"Content-Type": "text/plain",
"X-API-key": key,
},
});

const content = await response.json();
const detail = content.detail || content.message || content;

if (response.ok) {
client.remove();
await sendToast(`${detail}`, "alert-info");
return;
}

spinner.classList.add("hidden");

if (response.headers.get("content-type") !== "application/json") {
throw `unexpected response: ${response.statusText} ${response.status}`;
}

if (response.status === 404) {
await sendToast(
`Could not delete client for ${proposal_no} - ${detail}`,
"alert-warning",
);
} else {
throw `unexpected response: ${detail} (${response.statusText} ${response.status})`;
}
} catch (e) {
spinner.classList.add("hidden");
await sendToast(
`Error deleting client ${proposal_no} - ${e}`,
"alert-error",
);
}
}

var viewMessagesKey = null;
</script>
4 changes: 1 addition & 3 deletions src/zulip_write_only_proxy/frontend/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,5 @@

<!-- Content -->
{% block content %}
<a hx-boost="false" class="btn" href="{{ url_for('auth') }}"
>Login</a
>
<a hx-boost="false" class="btn" href="{{ url_for('auth') }}">Login</a>
{% endblock %}
10 changes: 10 additions & 0 deletions src/zulip_write_only_proxy/models/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ def __init__(self):
super().__init__(status_code=404, detail="No Zulip bot configured for client")


class NoStreamForClientError(ZwopException):
def __init__(self):
super().__init__(
status_code=404, detail="No Zulip stream configured for client"
)


class ScopedClientCreate(BaseModel):
proposal_no: int
stream: str | None = None
Expand Down Expand Up @@ -71,6 +78,9 @@ def upload_file(self, file: IO[Any]):
return self._client.upload_file(file)

def get_stream_topics(self):
if self.stream is None:
raise NoStreamForClientError

stream = self._client.get_stream_id(self.stream)
if stream["result"] != "success":
logger.error(
Expand Down
51 changes: 33 additions & 18 deletions src/zulip_write_only_proxy/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pydantic
from anyio import Path as APath

from . import logger
from . import exceptions, logger
from .models.base import Base

T = TypeVar("T", bound=Base)
Expand Down Expand Up @@ -55,33 +55,48 @@ async def write(self):
)
)

async def get(self, key: str | int, by: str | None = None) -> T | None:
res = None

if by:
for item in self._data:
k = getattr(item, by, None)
def _get_key_value(self, item, by):
k = getattr(item, by, None)
if isinstance(k, pydantic.SecretStr):
k = k.get_secret_value()
return k

if type(k) is pydantic.SecretStr:
k = k.get_secret_value()
def _get_by(self, by, key):
return next(
(item for item in self._data if self._get_key_value(item, by) == key),
None,
)

if k == key:
res = item
break
else:
res = self.data.get(str(key))
async def get(self, key: str | int, by: str | None = None) -> T | None:
res = self._get_by(by, key) if by else self.data.get(str(key))

if res is None:
logger.warning("Key not found", key=key, by=by)

return res

async def delete(self, key: str, by: str | None = None) -> str:
item = self._get_by(by, key) if by else self.data.get(key)

if item is None:
raise KeyError(by or "key", key)

_key = item._key

self._data.remove(item)
del self.data[_key]

await self.write()

return _key

async def insert(self, item: T):
if item._key in self.data:
msg = "Key already exists"
logger.warning(msg, key=item._key)
raise ValueError(msg)

logger.warning("Client already exists", key=item._key)
raise exceptions.ZwopException(
status_code=409,
detail=f"Client already exists for {item._key}",
)
self._data.append(item)
self.data[item._key] = self._data[-1]

Expand Down
Loading
Loading