Extensions

Extensions provide a way for publishers to deploy content that adds features to Connect without requiring server upgrades. These features might be interfaces to capabilities which are only currently supported via the Connect Server API (e.g. using an application to enable bulk content deletion.) To deploy an extension, you must package your content as a tarball (*.tar.gz or *.tgz) and then upload it to Connect. Once deployed, extensions can be found in the Content List and will respect the same access controls as other Connect content.

Prerequisites

  1. You must have Publisher or Administrator permissions to add an extension to a Connect instance.

Example: A simple extension to list the last ten active users

What if you wanted to list the last ten users to log in to your Connect instance? Connect does not support this feature directly. However, creating a Quarto document that uses the Connect public API to list the last ten users is straightforward. We can create this document and add it as an extension to Connect.

Preparing the Extension Contents

  1. Copy the following documents into your project directory:
---
title: "Last 10 Active Users"
---

```{python}
#| echo: false

from IPython.display import Markdown
from datetime import *
from posit.connect import Client
from tabulate import tabulate
import json
import os
import pytz

CONNECT_API_KEY = os.getenv('CONNECT_API_KEY')
CONNECT_SERVER = os.getenv('CONNECT_SERVER')

def sort_by_login(user):
  return user['active_time']

def find_users():
  with Client(
  api_key=CONNECT_API_KEY, url=CONNECT_SERVER
  ) as client:
    find_results = client.users.find()
    find_results.sort(key=sort_by_login, reverse=True)
    return find_results


def last_active_time(dt):
  utc_time = pytz.utc.localize(dt)
  local_timezone = pytz.timezone('US/Eastern')
  local_time = utc_time.astimezone(local_timezone)
  # return local_time.strftime("%Y-%m-%d %-I:%M %p")
  return local_time.strftime("%c")

recent_users = find_users()
display_users = []
for user in recent_users[:10]:
  local_time = datetime.strptime(user.active_time, '%Y-%m-%dT%H:%M:%SZ')

  display_users.append({
    'email': user.email,
    'name': f'{user.first_name} {user.last_name}',
    'username': user.username,
    'last_active': last_active_time(local_time)
  })

rows = [x.values() for x in display_users]

Markdown(tabulate( rows, headers=["Email", "Name", "Username", "Last Active"]))
```
{
  "version": 1,
  "locale": "en_US.UTF-8",
  "metadata": {
    "appmode": "quarto-static"
  },
  "quarto": {
    "version": "1.5.57",
    "engines": [
      "jupyter"
    ]
  },
  "python": {
    "version": "3.11.9",
    "package_manager": {
      "name": "pip",
      "version": "24.2",
      "package_file": "requirements.txt"
    }
  },
  "files": {
    "requirements.txt": {
      "checksum": "ff6c426754245786e5d5620c948ac68a"
    },
    "RecentUsers.qmd": {
      "checksum": "f2a7d7999f2cb1aac7d3925bbdc338c7"
    }
  }
}
name = "connect-extension-recent-users"
title = "Last 10 Active Users"
description = "Connect Extension: List last 10 active users"
access_type = "logged_in"
# requirements.txt generated by rsconnect-python on 2024-09-10 18:51:57.198908
aiofiles==23.2.1
altair==5.2.0
aniso8601==9.0.1
annotated-types==0.7.0
anyio==3.7.1
anywidget==0.9.2
appdirs==1.4.4
appnope==0.1.3
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asgiref==3.8.1
asttokens==2.4.1
attrs==23.1.0
beautifulsoup4==4.12.2
bleach==6.1.0
blinker==1.8.2
bokeh==3.4.0
build==1.2.1
cachetools==5.3.3
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.3.2
click==8.1.7
comm==0.2.0
commonmark==0.9.1
contourpy==1.2.1
cryptography==43.0.1
debugpy==1.8.0
decorator==5.1.1
defusedxml==0.7.1
distlib==0.3.7
entrypoints==0.4
executing==2.0.1
fastapi==0.103.1
fastapitableau==1.2.0
fastjsonschema==2.19.0
filelock==3.12.2
Flask==3.0.2
flask-restx==1.3.0
fqdn==1.5.1
fsspec==2024.5.0
fuzzbucket-client==0.12.2
gitdb==4.0.11
GitPython==3.1.43
h11==0.14.0
htmltools==0.5.2
httpcore==1.0.5
httpx==0.27.0
humanize==4.9.0
idna==3.6
importlib_metadata==7.1.0
importlib_resources==6.4.0
ipykernel==6.27.1
ipython==8.18.0
ipython-genutils==0.2.0
ipywidgets==8.1.1
isoduration==20.11.0
itsdangerous==2.2.0
jedi==0.19.1
Jinja2==3.1.2
joblib==1.3.2
jsonpointer==2.4
jsonschema==4.20.0
jsonschema-specifications==2023.11.2
jupyter==1.0.0
jupyter-console==6.6.3
jupyter-events==0.9.0
jupyter_client==7.4.9
jupyter_core==5.5.0
jupyter_server==2.12.1
jupyter_server_terminals==0.5.0
jupyterlab-widgets==3.0.9
jupyterlab_pygments==0.3.0
linkify-it-py==2.0.3
markdown-it-py==3.0.0
MarkupSafe==2.1.3
matplotlib-inline==0.1.6
mdit-py-plugins==0.4.1
mdurl==0.1.2
mistune==3.0.2
nbclassic==1.0.0
nbclient==0.9.0
nbconvert==7.12.0
nbformat==5.9.2
nest-asyncio==1.5.8
notebook==6.5.6
notebook_shim==0.2.3
numpy==1.26.4
overrides==7.4.0
packaging==23.2
panda==0.3.1
pandas==2.2.1
pandocfilters==1.5.0
parso==0.8.3
pexpect==4.9.0
pillow==10.3.0
pins==0.8.4
pip-tools==7.4.1
pipenv==2023.7.23
platformdirs==3.10.0
plotly==5.22.0
posit-sdk==0.4.0
prometheus-client==0.19.0
prompt-toolkit==3.0.36
protobuf==4.25.3
psutil==5.9.6
psygnal==0.11.1
ptyprocess==0.7.0
pure-eval==0.2.2
pyarrow==16.1.0
pycparser==2.22
pydantic==2.7.2
pydantic_core==2.18.3
pydeck==0.9.1
Pygments==2.17.2
PyJWT==2.8.0
pyproject_hooks==1.1.0
python-dateutil==2.8.2
python-dotenv==1.0.1
python-json-logger==2.0.7
python-multipart==0.0.9
pytz==2023.3.post1
PyYAML==6.0.1
pyzmq==24.0.1
qtconsole==5.5.1
QtPy==2.4.1
questionary==2.0.1
referencing==0.32.0
requests==2.31.0
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rich==13.7.1
rpds-py==0.13.2
scikit-learn==1.4.1.post1
scipy==1.13.1
semver==2.13.0
Send2Trash==1.8.2
shiny==1.0.0
shinyswatch==0.7.0
shinywidgets==0.3.1
six==1.16.0
smmap==5.0.1
sniffio==1.3.0
soupsieve==2.5
stack-data==0.6.3
starlette==0.27.0
streamlit==1.31.1
tabulate==0.9.0
tenacity==8.3.0
terminado==0.18.0
threadpoolctl==3.5.0
tinycss2==1.2.1
toml==0.10.2
toolz==0.12.1
tornado==6.4
traitlets==5.14.0
types-python-dateutil==2.8.19.14
typing_extensions==4.12.0
tzdata==2023.3
tzlocal==5.2
uc-micro-py==1.0.3
uri-template==1.3.0
urllib3==2.1.0
uvicorn==0.30.0
validators==0.28.3
vetiver==0.2.4
virtualenv==20.24.2
virtualenv-clone==0.5.7
watchfiles==0.22.0
wcwidth==0.2.12
webcolors==1.13
webencodings==0.5.1
websocket-client==1.7.0
websockets==12.0
Werkzeug==3.0.3
widgetsnbextension==4.0.9
xxhash==3.4.1
xyzservices==2024.4.0
zipp==3.19.1
Note

The files, requirements.txt and manifest.json, are included here to simplify the tutorial. If you are creating your own extension, you can generate these files with the commmand-line interface by calling rsconnect write-manifest or by calling rsconnect::writeManifest() in an R console.

  1. Create a tarball of the documents:
> tar -czvf connect-extension-recent-users.tar.gz RecentUsers.qmd manifest.json connect-extension.toml requirements.txt

Adding the Extension to Connect

  1. Visit the Connect home page

  2. Click the Publish button and select Add an Extension from the dropdown menu.

    Add an Extension menu

  3. In the Add Extension menu, select the Bundle tab.

    • if you are adding an extension that is publicly hosted, you can use the URL tab to add the extension by its URL.
  4. Drag and drop the connect-extension-recent-users.tar.gz file into the drop zone or click the Browse button to select the file from your local file system.

    Add Extension Modal

  5. Click the Add Extension button to upload the extension to Connect.

  6. The extension will be uploaded and published to your Connect instance. This might take a few minutes.

    Publishing the Extension

  7. Once the extension is published, you can Open it in Connect.

    Publishing complete

Viewing the Extension

  • Once published, extensions appear in the Content list with a badge indicating that they are extensions.

    Extension in Content list

  • Clicking on the extension opens it in Connect.

    Extension view

  • The extension is shared with all users by default. However, it can be restricted and managed just like any other content published on Connect.