Change of appearance of app is not visible everywhere

I modified the name, icon and picture of an app. This is visible in the “App store”, but in other locations (under “Projects”) it has retained the old appearance.

How can I make sure it appears everywhere in the desired way?

We have the same issue. It should be possible to link the workspaces icon/picture/description to the app store. So we can orchestrate all UI/styling from the app store.

Now it is simply impossible and a mess as all workspaces are unlinked.

Great request!

@thomasvdl Indeed, @Johan_Tuls is correct that this is not currently possible. The app as it is added to a project is a unique instance of the app, and has it’s own image and icon. You can edit them by finding the app in a project and editing it there, but it’s currently not possible to propagate this type of thing from the app “downward” into projects. I definitely agree with you that it would make sense for that to be possible though!

but this should be doable using the REST API right?
Do you mind sharing an example on how we could do that?

@Enrique your question inspired me, because indeed, you’d think it’s possible! So, I had to give it a shot, and after quite a lot of fiddling I got it to work.

The following script expects you to set an app_id and place an image of your choosing in the folder, then it will use the REST API to update the image for the app and all the workspaces attached to this app. NB: if you had multiple images attached to your app this script currently will overwrite that with the single new image. Hope you find it useful!

import base64
import os
import requests
from dotenv import load_dotenv
from pathlib import Path


load_dotenv()


def encode_image_to_base64(image_path):
    with open(image_path, "rb") as image_file:
        image_data = image_file.read()
        base64_encoded = base64.b64encode(image_data).decode("utf-8")
        return f"{base64_encoded}"


def main():
    # API token and headers to use in requests
    API_TOKEN = os.getenv("API_TOKEN")
    headers = {"Accept": "application/json", "Authorization": f"bearer {API_TOKEN}"}

    # New image for relevant app and attached workspaces
    image_path = Path(__file__).parent / "MY_NEW_IMAGE.jpg"  # TODO: update with your image
    base64_string = encode_image_to_base64(image_path)

    # Get app and update (first) image
    app_id = 2  # TODO: update to the id of your relevant app
    url = f"https://demo.viktor.ai/api/apps/{app_id}/"
    payload = {"images": [base64_string]}
    response = requests.request("PATCH", url, headers=headers, json=payload)
    print(f"Upload response for app {app_id}: {response.status_code}")

    # Get workspaces for this app
    url = "https://demo.viktor.ai/api/workspaces/"
    workspaces = requests.request("GET", url, headers=headers, data={})
    workspace_id_list = []
    for ws in workspaces.json():
        if ws["app"]["id"] == app_id:
            workspace_id_list.append(ws["id"])

    # Loop over workspaces and update image
    for ws_id in workspace_id_list:
        url = f"https://demo.viktor.ai/api/workspaces/{ws_id}/"
        payload = {"image": base64_string}
        response = requests.request("PATCH", url, headers=headers, json=payload)
        print(f"Upload response for workspace {ws_id}: {response.status_code}")


if __name__ == "__main__":
    main()

You are amazing @rweigand :flexed_biceps:

I will try this asap :blush:

1 Like

Thanks @rweigand Awesome to have this solution!

Is it also possible to set a new name and icon by using something similar to: payload = {"image": base64_string, "icon": another_base64_string, "name": "new name"} instead of payload = {"image": base64_string}?

@rweigand with this

    response = requests.request("PATCH", url, headers=headers, json=payload)
    print(f"Upload response for app {app_id}: {response.status_code}")

I get the result Upload response for app xx: 400 So there seems to be something wrong.
Does this request use the PAT as described in REST API | VIKTOR Documentation?

The resulting list of workspace ids from the next step seems to be alright.

Hi @thomasvdl, sure, you can also set other things besides the image. To answer your questions:

  • A 400 means “bad request”, so the request you sent is not formatted/schematized correctly
  • Indeed, these requests will required you to authenticate, so that why in the example script a Personal Access Token is read from the environment variables, and I used load_dotenv() in combination with a .env file that had the token. If you’re getting a 400 the authentication probably already worked though, otherwise you’d get a “not authorized” response (401)
  • The name can be set, the full response/schema of a workspace you can find either in the docs or by using the network inspector and clicking through the update yourself. name is definitely in there, however icon is not, i’m also not sure what you mean by that?

I think there might be an error in the suggested code. Using payload = {"image": base64_string} instead of payload = {"images": [base64_string]} gives a 200 response for the app. The workspaces still give a 400 response for a patch request with image with the same payload. Changing the name was successful.

The icon is wat is set here (in edit app, Dutch language version):

Well i did see i had accidentally double-pasted the code, have update the code block now. An app can have multiple images, as you can show a gallery of images in the app store, which is why i used the list. A workspace can only have a single image, which is why I only insert the one.

The icon it seems IS in the app schema ( Get App | VIKTOR Documentation ), so I think you might be able to PATCH that as well, in much the same way. Let me know if you can get that working or if you need me to try it.

Btw, here is an example of the schema of a demo app, these are all the things you can send via a PATCH for an app:

1 Like

Thank you very much @rweigand! The image I used was too large :blush: I just read the image (or the base64_string?) can be maximum 1 MB. Reducing the size solved the issue :+1:

Ah yes that’s indeed a good catch, there are restrctions for both the image as well as the icon. You mention 1 MB, and if that works it works, I don’t know the restrictions by heart but if you go through the edit modal in the VIKTOR environment the mentioned restrictions are:

  • Image: The image can be a JPG/JPEG or PNG file with a maximum size of 600 KB.
  • Icon: The image must be a JPG/JPEG or PNG file with a maximum size of 100 KB and will be displayed as 40x40 pixels.

So that’s less than the 1 MB you mention!?

1 Like

And for the sake of complete-ness, this is the schema of a workspace that you can send via PATCH:

image

1 Like

Thanks for the input and great that Roeland could help you with this “hack”!

Out of curiosity: do you see a situation where you do want another icon/picture for an app in a specific project or can we just always use the icon/picture from the app?

cc: @tvantil @mslootweg

Guys that for the input. I used it to make something that we wanted to have.

Each app’s first image as the image for every workspace of that app.

"""Change the image of a workspace to the first image of the app it belongs to."""

import base64
import os
import requests
from pathlib import Path


# Use your own
viktor_token = "vktrpat_XXXXXXXXXXXX"  # TODO: update with your API token
base_url = "your-company.viktor.ai"


def get_response_from_viktor_api(
    endpoint: str,
    method: str = "GET",
    data: dict = None,
) -> dict:
    """Helper function to make requests to the Viktor API.

    Parameters
    ----------
    endpoint : str
        The API endpoint to call (e.g., "/api/apps/").
    method : str, optional
        The HTTP method to use (default is "GET").
    data : dict, optional
        The data to send in the request body for POST/PATCH requests.

    Returns
    -------
    dict
        The JSON response from the API as a dictionary.

    Raises
    ------
    requests.HTTPError
        If the API request fails with an HTTP error.
    """
    url = f"https://{base_url}{endpoint}"
    headers = {
        "Accept": "application/json",
        "Authorization": f"Bearer {viktor_token}",
    }

    response = requests.request(method=method, url=url, headers=headers, json=data)
    response.raise_for_status()  # Raise an error for bad responses

    return response.json()


def get_viktor_app_info(app_id: int) -> dict:
    """Get app information from Viktor API.

    Parameters
    ----------
    app_id : int
        The ID of the app to retrieve information for.

    Returns
    -------
    dict
        A dictionary containing the app information, including name, description, images, icon etc.
        Check Viktor's API documentation for the exact structure of the response.
        https://docs.viktor.ai/docs/api/rest/apps-read/

    """
    # Get basic app info
    app_info = get_response_from_viktor_api(endpoint=f"/api/apps/{app_id}/")

    # Get the app's images and add that to the basic info
    images_info = get_response_from_viktor_api(endpoint=f"/api/apps/{app_id}/images/")
    app_info["images"] = [image.split("base64,")[1] for image in images_info]

    # let's add the app icon as well
    icon_info = get_response_from_viktor_api(endpoint=f"/api/apps/{app_id}/icon/")
    assert isinstance(icon_info, str)
    app_info["icon"] = icon_info.split("base64,")[1]

    return app_info


def get_app_workspaces(app_id: int) -> list[int]:
    """Get workspaces for a given app.

    Parameters
    ----------
    app_id : int
        The ID of the app to retrieve workspaces for.

    Returns
    -------
    list[int]
        A list of workspace IDs that are associated with the given app.
    """
    workspaces = get_response_from_viktor_api(endpoint="/api/workspaces/")
    return [ws["id"] for ws in workspaces if ws["app"]["id"] == app_id]


def change_workspace_image(workspace_id: int, base64_image: str) -> None:
    """Change the image of a workspace.

    Parameters
    ----------
    workspace_id : int
        The ID of the workspace to update.
    base64_image : str
        The new image encoded as a base64 string.

    Returns
    -------
    None

    Raises
    ------
    requests.HTTPError
        If the API request fails with an HTTP error.
    """
    endpoint = f"/api/workspaces/{workspace_id}/"
    data = {"image": base64_image}
    get_response_from_viktor_api(endpoint=endpoint, method="PATCH", data=data)


def encode_image_to_base64(image_path: str) -> str:
    """Encode an image file to a base64 string.

    Parameters
    ----------
    image_path : str
        The file path to the image to be encoded.

    Returns
    -------
    str
        The base64-encoded string representation of the image.
    """
    with open(image_path, "rb") as image_file:
        image_data = image_file.read()
        base64_encoded = base64.b64encode(image_data).decode("utf-8")
        return f"{base64_encoded}"


def first_image_of_app_to_workspaces(app_id: int) -> None:
    """Get the first image of an app and set it as the workspace image for all workspaces of that app.

    Parameters
    ----------
    app_id : int
        The ID of the app to retrieve the first image from and update workspaces for.

    Returns
    -------
    None

    Raises
    ------
    requests.HTTPError
        If any of the API requests fail with an HTTP error.
    """
    # Get app info including images
    info = get_viktor_app_info(app_id=app_id)

    # find which workspaces belong to the app
    workspaces = get_app_workspaces(app_id=app_id)

    # get the first image of the app
    first_image = info['images'][0]

    # Loop over workspaces and update image
    for ws_id in workspaces:
        change_workspace_image(workspace_id=ws_id, base64_image=first_image)
        print(f"Updated workspace {ws_id} with the first image of app {app_id}")


if __name__ == "__main__":
    first_image_of_app_to_workspaces(app_id=2) # TODO: update to the id of your relevant app
    
1 Like

For us I don’t see a situation where we would like to use different pictures or icons in specific projects. But there might be a business case for instance for an independent developer who also hosts the apps and wants to sell the same app to different clients as a “custom” development, which is proven by the client’s logo.